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

import {
  CancellationToken,
  FailCasesByMethod,
  HttpStatusCode,
  LibtoolVersion,
  OperationFail,
  OperationOk,
  ResultByMethod,
  TalerMailboxApi,
  carefullyParseConfig,
  codecForTalerMailboxConfigResponse,
  codecForTalerMailboxRateLimitedResponse,
  opEmptySuccess,
  opFixedSuccess,
  opKnownAlternativeHttpFailure,
  opKnownHttpFailure,
  opUnknownHttpFailure,
  MailboxMetadata,
  codecForTalerMailboxMetadata,
  opSuccessFromHttp,
  MailboxRegisterRequest,
  encodeCrock,
  decodeCrock,
  codecForEmptyObject,
  MailboxConfiguration,
  eddsaGetPublic,
  EddsaSignatureString,
  MailboxRegisterResult,
  OperationAlternative,
} from "@gnu-taler/taler-util";
import {
  HttpRequestLibrary,
  createPlatformHttpLib,
} from "@gnu-taler/taler-util/http";

export type TalerMailboxInstanceResultByMethod<
  prop extends keyof TalerMailboxInstanceHttpClient,
> = ResultByMethod<TalerMailboxInstanceHttpClient, prop>;
export type TalerMailboxInstanceErrorsByMethod<
  prop extends keyof TalerMailboxInstanceHttpClient,
> = FailCasesByMethod<TalerMailboxInstanceHttpClient, prop>;

export interface MailboxMessagesResponseRaw {
  messages: Uint8Array;
  etag: string;
}

/**
 * Protocol version spoken with the service.
 *
 * Endpoint must be ordered in the same way that in the docs
 * Response code (http and taler) must have the same order that in the docs
 * That way is easier to see changes
 *
 * Uses libtool's current:revision:age versioning.
 */
export class TalerMailboxInstanceHttpClient {
  public static readonly PROTOCOL_VERSION = "1:0:0";

  readonly httpLib: HttpRequestLibrary;
  readonly cancellationToken: CancellationToken | undefined;

  constructor(
    readonly baseUrl: string,
    httpClient?: HttpRequestLibrary,
    cancellationToken?: CancellationToken,
  ) {
    this.httpLib = httpClient ?? createPlatformHttpLib();
    this.cancellationToken = cancellationToken;
  }

  static isCompatible(version: string): boolean {
    const compare = LibtoolVersion.compare(
      TalerMailboxInstanceHttpClient.PROTOCOL_VERSION,
      version,
    );
    return compare?.compatible ?? false;
  }

  /**
   * https://docs.taler.net/core/api-mailbox.html#get--config
   */
  async getConfig() :
    Promise<
      | OperationOk<TalerMailboxApi.TalerMailboxConfigResponse>
    | OperationFail<HttpStatusCode.NotFound>
  >{
    const url = new URL(`/config`, this.baseUrl);
    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
    });
    switch (resp.status) {
      case HttpStatusCode.Ok:
        return carefullyParseConfig(
          "taler-mailbox",
          TalerMailboxInstanceHttpClient.PROTOCOL_VERSION,
          resp,
          codecForTalerMailboxConfigResponse(),
        );
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-mailbox.html#post--$H_MAILBOX
   */
  async sendMessage(args: {
    h_address: string;
    body: Uint8Array;
  }): Promise<
    | OperationOk<void>
    | OperationFail<TalerMailboxApi.MailboxRateLimitedResponse>
    | OperationFail<HttpStatusCode.PaymentRequired>
    | OperationFail<HttpStatusCode.TooManyRequests>
    | OperationFail<HttpStatusCode.Forbidden>
  > {
    const { h_address: hAddress, body } = args;
    const url = new URL(`${hAddress.toUpperCase()}`, this.baseUrl);

    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      body,
      cancellationToken: this.cancellationToken,
    });

    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        return opEmptySuccess();
      }
      case HttpStatusCode.PaymentRequired: {
        return opKnownHttpFailure(resp.status, resp);
      }
      case HttpStatusCode.Forbidden: {
        return opKnownHttpFailure(resp.status, resp);
      }
      case HttpStatusCode.TooManyRequests: {
        return opKnownAlternativeHttpFailure(resp, resp.status, codecForTalerMailboxRateLimitedResponse());
      }
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-mailbox.html#get--$H_MAILBOX
   */
  async getMessages(args: {
    hMailbox: string;
  }): Promise<
    | OperationOk<MailboxMessagesResponseRaw>
    | OperationOk<void>
    | OperationFail<HttpStatusCode.TooManyRequests>
  >{
    const { hMailbox: hMailbox } = args;
    const url = new URL(`${hMailbox.toUpperCase()}`, this.baseUrl);

    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      cancellationToken: this.cancellationToken,
    });

    switch (resp.status) {
      case HttpStatusCode.Ok: {
        const uintar = await resp.bytes() as Uint8Array;
        const etag = resp.headers.get("etag")
        const index = etag? etag : "0";
        return opFixedSuccess({messages: uintar, etag: index});
      }
      case HttpStatusCode.NoContent: {
        return opEmptySuccess();
      }
      case HttpStatusCode.TooManyRequests: {
        return opKnownAlternativeHttpFailure(resp, resp.status, codecForTalerMailboxRateLimitedResponse());
      }
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-mailbox.html#delete--$ADDRESS
   */
  async deleteMessages(args: {
    mailboxConf: MailboxConfiguration;
    matchIf: string;
    count: number;
    signature: EddsaSignatureString;
  }): Promise<
    | OperationOk<void>
    | OperationFail<HttpStatusCode.Forbidden>
    | OperationFail<HttpStatusCode.NotFound>
   >{
    const { mailboxConf, matchIf: etag, count: count, signature: signature } = args;
    const mailboxPubkeyString = encodeCrock(eddsaGetPublic(decodeCrock(mailboxConf.privateKey)));
    const url = new URL(`${mailboxPubkeyString.toUpperCase()}?count=${count}`, this.baseUrl);


    const resp = await this.httpLib.fetch(url.href, {
      method: "DELETE",
      headers: {
        "If-Match" : etag,
        "Taler-Mailbox-Delete-Signature" : signature},
      cancellationToken: this.cancellationToken,
    });

    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        return opEmptySuccess();
      }
      case HttpStatusCode.Forbidden:
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
       default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-mailbox.html#get--info-$H_MAILBOX
   */
  async getMailboxInfo(hMailbox: string) : Promise<
    | OperationOk<MailboxMetadata>
   | OperationFail<HttpStatusCode.NotFound>
   | OperationFail<HttpStatusCode.TooManyRequests>
  >{
    const url = new URL(`info/${hMailbox.toUpperCase()}`, this.baseUrl);

    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      cancellationToken: this.cancellationToken,
    });

    switch (resp.status) {
      case HttpStatusCode.Ok: {
        return opSuccessFromHttp(resp,
                                 codecForTalerMailboxMetadata());
      }
      case HttpStatusCode.NotFound: {
        return opKnownAlternativeHttpFailure(resp, resp.status, codecForEmptyObject());
      }
      case HttpStatusCode.TooManyRequests: {
        return opKnownAlternativeHttpFailure(resp, resp.status, codecForTalerMailboxRateLimitedResponse());
      }
     default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-mailbox.html#post--register
   */
  async registerMailbox(req: MailboxRegisterRequest) : Promise<
    | OperationOk<MailboxRegisterResult>
    | OperationFail<HttpStatusCode.Forbidden>
    | OperationAlternative<HttpStatusCode.PaymentRequired, MailboxRegisterResult>
  >{
    const url = new URL(`register`, this.baseUrl);

    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      body: req,
      cancellationToken: this.cancellationToken,
    });

    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        return opFixedSuccess({status: "ok"} as MailboxRegisterResult);
      }
      case HttpStatusCode.Forbidden: {
        return opKnownHttpFailure(resp.status, resp);
      }
      case HttpStatusCode.PaymentRequired: {
        return { type: "fail", case: resp.status,  body: { status: "payment-required", talerUri: resp.headers.get("Taler")} as MailboxRegisterResult};
      }
      default:
        return opUnknownHttpFailure(resp);
    }
  }
}
