/*
 This file is part of GNU Taler
 (C) 2023-2024 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 {
  HttpResponse,
  readResponseJsonOrErrorCode,
  readSuccessResponseJsonOrThrow,
  readTalerErrorResponse,
} from "./http-common.js";
import {
  Codec,
  HttpStatusCode,
  TalerError,
  TalerErrorCode,
  TalerErrorDetail,
} from "./index.js";

type OperationFailWithBodyOrNever<ErrorEnum, ErrorMap> =
  ErrorEnum extends keyof ErrorMap ? OperationFailWithBody<ErrorMap> : never;

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

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";

  /**
   * 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";

  case: T;
  body: B;
}

export interface OperationFailWithBody<B> {
  type: "fail";

  case: keyof B;
  body: B[OperationFailWithBody<B>["case"]];
}

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

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

export async function opKnownFailureWithBody<B>(
  case_: keyof B,
  body: B[typeof case_],
): Promise<OperationFailWithBody<B>> {
  return { type: "fail", case: case_, body };
}

export async function opKnownAlternativeFailure<T extends HttpStatusCode, B>(
  resp: HttpResponse,
  s: T,
  codec: Codec<B>,
): Promise<OperationAlternative<T, B>> {
  const body = (await readResponseJsonOrErrorCode(resp, codec)).response;
  return { type: "fail", case: s, body };
}

export async function opKnownHttpFailure<T extends HttpStatusCode>(
  s: T,
  resp: HttpResponse,
): Promise<OperationFail<T>> {
  const detail = await readTalerErrorResponse(resp);
  return { type: "fail", case: s, detail };
}

export function opKnownTalerFailure<T extends TalerErrorCode>(
  s: T,
  detail: TalerErrorDetail,
): OperationFail<T> {
  return { type: "fail", case: s, detail };
}

export function opUnknownFailure(
  resp: HttpResponse,
  error: TalerErrorDetail,
): never {
  throw TalerError.fromDetail(
    TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
    {
      requestUrl: resp.requestUrl,
      requestMethod: resp.requestMethod,
      httpStatusCode: resp.status,
      errorResponse: error,
    },
    `Unexpected HTTP status ${resp.status} in response`,
  );
}

/**
 * Convenience function to throw an error if the operation is not a success.
 */
export function narrowOpSuccessOrThrow<Body, ErrorEnum>(
  opName: string,
  opRes: OperationResult<Body, ErrorEnum>,
): asserts opRes is OperationOk<Body> {
  if (opRes.type !== "ok") {
    throw TalerError.fromDetail(
      TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR,
      {
        operation: opName,
        error: String(opRes.case),
        detail: "detail" in opRes ? opRes.detail : undefined,
      },
      `Operation ${opName} failed: ${String(opRes.case)}`,
    );
  }
}

export async function succeedOrThrow<R, E>(
  promise: Promise<OperationResult<R, E>>,
): Promise<R> {
  const resp = await promise;
  if (isOperationOk(resp)) {
    return resp.body;
  }

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

export async function failOrThrow<E>(
  s: E,
  promise: Promise<OperationResult<unknown, E>>,
): Promise<TalerErrorDetail | undefined> {
  const resp = await promise;
  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>
>;

export type RedirectResult = { redirectURL: URL };
