/*
 This file is part of GNU Taler
 (C) 2023-2025 Taler Systems S.A.

 GNU Taler is free software; you can redistribute it and/or modify it under the
 terms of the GNU General Public License as published by the Free Software
 Foundation; either version 3, or (at your option) any later version.

 GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 A PARTICULAR PURPOSE.  See the GNU General Public License for more details.

 You should have received a copy of the GNU General Public License along with
 GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 */

/**
 * Imports.
 */
import { Codec } from "./codec.js";
import { TalerError } from "./errors.js";
import {
  HttpResponse,
  readResponseJsonOrThrow,
  readSuccessResponseJsonOrThrow,
  readTalerErrorResponse,
} from "./http-common.js";
import { HttpStatusCode } from "./http-status-codes.js";
import { LibtoolVersion } from "./libtool-version.js";
import { TalerErrorCode } from "./taler-error-codes.js";
import { codecForTalerCommonConfigResponse } from "./types-taler-common.js";
import { TalerErrorDetail } from "./types-taler-wallet.js";

export type OperationResult<Body, ErrorEnum> =
  | OperationOk<Body>
  | OperationAlternative<ErrorEnum, any>
  | OperationFail<ErrorEnum>;

export function isOperationOk<T, E>(
  c: OperationResult<T, E>,
): c is OperationOk<T> {
  return c.type === "ok";
}

export function isOperationFail<T, E>(
  c: OperationResult<T, E>,
): c is OperationFail<E> {
  return c.type === "fail";
}

/**
 * successful operation
 */
export interface OperationOk<BodyT> {
  type: "ok";

  case: "ok";

  /**
   * Parsed response body.
   */
  body: BodyT;
}

/**
 * unsuccessful operation, see details
 */
export interface OperationFail<T> {
  type: "fail";

  /**
   * Error case (either HTTP status code or TalerErrorCode)
   */
  case: T;

  detail?: TalerErrorDetail;
}

/**
 * unsuccessful operation, see body
 */
export interface OperationAlternative<T, B> {
  type: "fail";

  /**
   * Either a HTTP status code or Taler error code to distinguish
   * the response type.
   */
  case: T;

  body: B;
}

export async function opSuccessFromHttp<T>(
  resp: HttpResponse,
  codec: Codec<T>,
): Promise<OperationOk<T>> {
  const body = await readSuccessResponseJsonOrThrow(resp, codec);
  return { type: "ok" as const, case: "ok", body };
}

/**
 * Success case, but instead of the body we're returning a fixed response
 * to the client.
 */
export function opFixedSuccess<T>(body: T): OperationOk<T> {
  return { type: "ok" as const, case: "ok", body };
}

export function opEmptySuccess(): OperationOk<void> {
  return { type: "ok" as const, case: "ok", body: void 0 };
}

export function opKnownFailure<const T>(case_: T): OperationFail<T> {
  return { type: "fail", case: case_ };
}

export function opKnownFailureWithBody<const T, const B>(
  case_: T,
  body: B,
): OperationAlternative<T, B> {
  return { type: "fail", case: case_, body };
}

/**
 * Before using the codec, try read minimum json body and
 * verify that component and version matches.
 *
 * @param expectedName
 * @param clientVersion
 * @param httpResponse
 * @param codec
 * @returns
 */
export async function carefullyParseConfig<T>(
  expectedName: string,
  clientVersion: string,
  httpResponse: HttpResponse,
  codec: Codec<T>,
) {
  const minBody = await readSuccessResponseJsonOrThrow(
    httpResponse,
    codecForTalerCommonConfigResponse(),
  );
  if (minBody.name !== expectedName) {
    throw TalerError.fromUncheckedDetail({
      code: TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR,
      requestUrl: httpResponse.requestUrl,
      httpStatusCode: httpResponse.status,
      detail: `Unexpected server component name (got ${minBody.name}, expected ${expectedName}})`,
    });
  }

  if (!LibtoolVersion.compare(clientVersion, minBody.version)) {
    throw TalerError.fromUncheckedDetail({
      code: TalerErrorCode.GENERIC_CLIENT_UNSUPPORTED_PROTOCOL_VERSION,
      requestUrl: httpResponse.requestUrl,
      httpStatusCode: httpResponse.status,
      detail: `Unsupported protocol version, client supports ${clientVersion}, server supports ${minBody.version}`,
    });
  }
  // Now that we've checked the basic body, re-parse the full response.
  const body = await readSuccessResponseJsonOrThrow(httpResponse, codec);
  return opFixedSuccess(body);
}

/**
 *
 * @param resp
 * @param s
 * @param codec
 * @returns
 */
export async function opKnownAlternativeHttpFailure<
  T extends HttpStatusCode,
  B,
>(
  resp: HttpResponse,
  s: T,
  codec: Codec<B>,
): Promise<OperationAlternative<T, B>> {
  const body = await readResponseJsonOrThrow(resp, codec);
  return { type: "fail", case: s, body };
}

/**
 * Constructor of a failure response of the API that is already documented in the spec.
 * The `case` parameter is a reason of the error.
 *
 * @param case
 * @param resp
 * @returns
 */
export async function opKnownHttpFailure<T extends HttpStatusCode>(
  _case: T,
  resp: HttpResponse,
  detail?: TalerErrorDetail,
): Promise<OperationFail<T>> {
  if (!detail) {
    detail = await readTalerErrorResponse(resp);
  }
  return { type: "fail", case: _case, detail };
}

/**
 * Constructor of an unexpected error, usually when the response of the API
 * is not in the spec.
 *
 * If the response hasn't already been read, this function will add the information
 * as detail
 *
 * @param resp
 * @param detail
 */
export async function opUnknownHttpFailure(
  resp: HttpResponse,
  detail?: TalerErrorDetail,
): Promise<never> {
  if (!detail) {
    detail = await readTalerErrorResponse(resp);
  }
  throw TalerError.fromDetail(
    TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
    {
      requestUrl: resp.requestUrl,
      requestMethod: resp.requestMethod,
      httpStatusCode: resp.status,
      errorResponse: detail,
    },
    `Unexpected HTTP status ${resp.status} in response`,
  );
}

/**
 * Constructor of a failure response of the API that is already documented in the spec.
 * The `case` parameter is a reason of the error.
 *
 * @param case
 * @param resp
 * @returns
 */
export function opKnownTalerFailure<T extends TalerErrorCode>(
  _case: T,
  detail: TalerErrorDetail,
): OperationFail<T> {
  return { type: "fail", case: _case, detail };
}

export function opUnknownFailure(error: unknown): never {
  throw TalerError.fromException(error);
}

/**
 * The operation result should be ok
 * Return the body of the result
 *
 * @param resp
 * @returns
 */
export function succeedOrThrow<R>(resp: OperationResult<R, unknown>): R {
  if (isOperationOk(resp)) {
    return resp.body;
  }

  if (isOperationFail(resp)) {
    throw TalerError.fromUncheckedDetail({ ...resp, case: resp.case } as any);
  }
  throw TalerError.fromException(resp);
}

export function succeedOrValue<R, V>(
  resp: OperationResult<R, unknown>,
  v: V,
): R | V {
  if (isOperationOk(resp)) {
    return resp.body;
  }

  return v;
}

/**
 * The operation is expected to fail with a body.
 * Return the body of the result.
 * Throw if the operation didn't fail with expected code.
 *
 * @param resp
 * @param s
 * @returns
 */
export function alternativeOrThrow<Error, Body, Alt>(
  resp:
    | OperationOk<Body>
    | OperationAlternative<Error, Alt>
    | OperationFail<Error>,
  s: Error,
): Alt {
  if (isOperationOk(resp)) {
    throw TalerError.fromException(
      new Error(`request succeed but failure "${s}" was expected`),
    );
  }
  if (isOperationFail(resp) && resp.case !== s) {
    throw TalerError.fromException(
      new Error(
        `request failed with "${JSON.stringify(
          resp,
        )}" but case "${s}" was expected`,
      ),
    );
  }
  return (resp as any).body;
}

/**
 * The operation is expected to fail.
 * Return the error details.
 * Throw if the operation didn't fail with expected code.
 *
 * @param resp
 * @param s
 * @returns
 */
export function failOrThrow<E>(
  resp: OperationResult<unknown, E>,
  s: E,
): TalerErrorDetail | undefined {
  if (isOperationOk(resp)) {
    throw TalerError.fromException(
      new Error(`request succeed but failure "${s}" was expected`),
    );
  }
  if (isOperationFail(resp) && resp.case === s) {
    return resp.detail;
  }
  throw TalerError.fromException(
    new Error(
      `request failed with "${JSON.stringify(
        resp,
      )}" but case "${s}" was expected`,
    ),
  );
}

export type ResultByMethod<
  TT extends object,
  p extends keyof TT,
> = TT[p] extends (...args: any[]) => infer Ret
  ? Ret extends Promise<infer Result>
    ? Result extends OperationResult<any, any>
      ? Result
      : never
    : never //api always use Promises
  : never; //error cases just for functions

export type FailCasesByMethod<TT extends object, p extends keyof TT> = Exclude<
  ResultByMethod<TT, p>,
  OperationOk<any>
>;
