/*
 This file is part of GNU Taler
 (C) 2023 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/>
 */

import { codecForAny } from "./codec.js";
import { HttpStatusCode } from "./http-status-codes.js";
import {
  createPlatformHttpLib,
  expectSuccessResponseOrThrow,
  readSuccessResponseJsonOrThrow,
  readTalerErrorResponse,
} from "./http.js";
import { LibtoolVersion } from "./libtool-version.js";
import { Logger } from "./logging.js";
import {
  FailCasesByMethod,
  OperationFail,
  OperationOk,
  ResultByMethod,
  opEmptySuccess,
  opKnownHttpFailure,
  opSuccessFromHttp,
  opUnknownFailure,
} from "./operation.js";
import { TalerProtocolDuration } from "./time.js";
import { AmountString } from "./types-taler-common.js";
import {
  OtpDeviceAddDetails,
  codecForTalerMerchantConfigResponse,
  codecForMerchantOrderPrivateStatusResponse,
  codecForPostOrderResponse,
} from "./types-taler-merchant.js";

import * as TalerMerchantApi from "./types-taler-merchant.js";

const logger = new Logger("MerchantApiClient.ts");

// FIXME: Explain!
export type TalerMerchantResultByMethod<prop extends keyof MerchantApiClient> =
  ResultByMethod<MerchantApiClient, prop>;

// FIXME: Explain!
export type TalerMerchantErrorsByMethod<prop extends keyof MerchantApiClient> =
  FailCasesByMethod<MerchantApiClient, prop>;

export interface MerchantAuthConfiguration {
  method: "external" | "token";
  token?: string;
}

// FIXME: Why do we need this? Describe / fix!
export interface PartialMerchantInstanceConfig {
  auth?: MerchantAuthConfiguration;
  id: string;
  name: string;
  paytoUris: string[];
  address?: unknown;
  jurisdiction?: unknown;
  defaultWireTransferDelay?: TalerProtocolDuration;
  defaultPayDelay?: TalerProtocolDuration;
}

export interface CreateMerchantTippingReserveRequest {
  // Amount that the merchant promises to put into the reserve
  initial_balance: AmountString;

  // Exchange the merchant intends to use for tipping
  exchange_url: string;

  // Desired wire method, for example "iban" or "x-taler-bank"
  wire_method: string;
}

export interface DeleteTippingReserveArgs {
  reservePub: string;
  purge?: boolean;
}

export interface MerchantInstanceConfig {
  auth: MerchantAuthConfiguration;
  id: string;
  name: string;
  address: unknown;
  jurisdiction: unknown;
  use_stefan: boolean;
  default_wire_transfer_delay: TalerProtocolDuration;
  default_pay_delay: TalerProtocolDuration;
}

export interface PrivateOrderStatusQuery {
  instance?: string;
  orderId: string;
  sessionId?: string;
}

/**
 * Client for the GNU Taler merchant backend.
 */
export class MerchantApiClient {
  /**
   * Base URL for the particular instance that this merchant API client
   * is for.
   */
  private baseUrl: string;

  readonly auth: MerchantAuthConfiguration;

  public readonly PROTOCOL_VERSION = "6:0:2";

  constructor(
    baseUrl: string,
    options: { auth?: MerchantAuthConfiguration } = {},
  ) {
    this.baseUrl = baseUrl;

    this.auth = options?.auth ?? {
      method: "external",
    };
  }

  httpClient = createPlatformHttpLib();

  async changeAuth(auth: MerchantAuthConfiguration): Promise<void> {
    const url = new URL("private/auth", this.baseUrl);
    const res = await this.httpClient.fetch(url.href, {
      method: "POST",
      body: auth,
      headers: this.makeAuthHeader(),
    });
    await expectSuccessResponseOrThrow(res);
  }

  async getPrivateInstanceInfo(): Promise<any> {
    const url = new URL("private", this.baseUrl);
    const resp = await this.httpClient.fetch(url.href, {
      method: "GET",
      headers: this.makeAuthHeader(),
    });
    return await resp.json();
  }

  async deleteInstance(instanceId: string) {
    const url = new URL(`management/instances/${instanceId}`, this.baseUrl);
    const resp = await this.httpClient.fetch(url.href, {
      method: "DELETE",
      headers: this.makeAuthHeader(),
    });
    await expectSuccessResponseOrThrow(resp);
  }

  async createInstance(req: MerchantInstanceConfig): Promise<void> {
    const url = new URL("management/instances", this.baseUrl);
    await this.httpClient.fetch(url.href, {
      method: "POST",
      body: req,
      headers: this.makeAuthHeader(),
    });
  }

  async getInstances(): Promise<TalerMerchantApi.InstancesResponse> {
    const url = new URL("management/instances", this.baseUrl);
    const resp = await this.httpClient.fetch(url.href, {
      headers: this.makeAuthHeader(),
    });
    return readSuccessResponseJsonOrThrow(resp, codecForAny());
  }

  async getInstanceFullDetails(instanceId: string): Promise<any> {
    const url = new URL(`management/instances/${instanceId}`, this.baseUrl);
    try {
      const resp = await this.httpClient.fetch(url.href, {
        headers: this.makeAuthHeader(),
      });
      return resp.json();
    } catch (e) {
      throw e;
    }
  }

  async createOrder(
    req: TalerMerchantApi.PostOrderRequest,
  ): Promise<TalerMerchantApi.PostOrderResponse> {
    let url = new URL("private/orders", this.baseUrl);
    const resp = await this.httpClient.fetch(url.href, {
      method: "POST",
      body: req,
      headers: this.makeAuthHeader(),
    });
    return readSuccessResponseJsonOrThrow(resp, codecForPostOrderResponse());
  }

  async deleteOrder(req: { orderId: string; force?: boolean }): Promise<void> {
    let url = new URL(`private/orders/${req.orderId}`, this.baseUrl);
    if (req.force) {
      url.searchParams.set("force", "yes");
    }
    const resp = await this.httpClient.fetch(url.href, {
      method: "DELETE",
      body: req,
      headers: this.makeAuthHeader(),
    });
    if (resp.status !== 204) {
      throw Error(`failed to delete order (status ${resp.status})`);
    }
  }

  async queryPrivateOrderStatus(
    query: PrivateOrderStatusQuery,
  ): Promise<TalerMerchantApi.MerchantOrderStatusResponse> {
    const reqUrl = new URL(`private/orders/${query.orderId}`, this.baseUrl);
    if (query.sessionId) {
      reqUrl.searchParams.set("session_id", query.sessionId);
    }
    const resp = await this.httpClient.fetch(reqUrl.href, {
      headers: this.makeAuthHeader(),
    });
    return readSuccessResponseJsonOrThrow(
      resp,
      codecForMerchantOrderPrivateStatusResponse(),
    );
  }

  async giveRefund(r: {
    instance: string;
    orderId: string;
    amount: string;
    justification: string;
  }): Promise<{ talerRefundUri: string }> {
    const reqUrl = new URL(`private/orders/${r.orderId}/refund`, this.baseUrl);
    const resp = await this.httpClient.fetch(reqUrl.href, {
      method: "POST",
      body: {
        refund: r.amount,
        reason: r.justification,
      },
    });
    const respBody = await resp.json();
    return {
      talerRefundUri: respBody.taler_refund_uri,
    };
  }

  async createTemplate(req: TalerMerchantApi.MerchantTemplateAddDetails) {
    let url = new URL("private/templates", this.baseUrl);
    const resp = await this.httpClient.fetch(url.href, {
      method: "POST",
      body: req,
      headers: this.makeAuthHeader(),
    });
    switch (resp.status) {
      case HttpStatusCode.Ok:
      case HttpStatusCode.NoContent:
        return opEmptySuccess(resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownFailure(resp, await readTalerErrorResponse(resp));
    }
  }

  async getTemplate(templateId: string) {
    let url = new URL(`private/templates/${templateId}`, this.baseUrl);
    const resp = await this.httpClient.fetch(url.href, {
      method: "GET",
      headers: this.makeAuthHeader(),
    });
    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForAny());
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownFailure(resp, await readTalerErrorResponse(resp));
    }
  }

  isCompatible(version: string): boolean {
    const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version);
    return compare?.compatible ?? false;
  }
  /**
   * https://docs.taler.net/core/api-merchant.html#get--config
   *
   */
  async getConfig(): Promise<OperationOk<TalerMerchantApi.TalerMerchantConfigResponse>> {
    const url = new URL(`config`, this.baseUrl);
    const resp = await this.httpClient.fetch(url.href, {
      method: "GET",
    });
    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForTalerMerchantConfigResponse());
      default:
        return opUnknownFailure(resp, await readTalerErrorResponse(resp));
    }
  }

  async createOtpDevice(
    req: OtpDeviceAddDetails,
  ): Promise<OperationOk<void> | OperationFail<HttpStatusCode.NotFound>> {
    let url = new URL("private/otp-devices", this.baseUrl);
    const resp = await this.httpClient.fetch(url.href, {
      method: "POST",
      body: req,
      headers: this.makeAuthHeader(),
    });
    switch (resp.status) {
      case HttpStatusCode.Ok:
      case HttpStatusCode.NoContent:
        return opEmptySuccess(resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownFailure(resp, await readTalerErrorResponse(resp));
    }
  }

  private makeAuthHeader(): Record<string, string> {
    switch (this.auth.method) {
      case "external":
        return {};
      case "token":
        return {
          Authorization: `Bearer ${this.auth.token}`,
        };
    }
  }
}
