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

 GNU Taler is free software; you can redistribute it and/or modify it under the
 terms of the GNU Affero 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 Affero General Public License along with
 GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>

 SPDX-License-Identifier: AGPL-3.0-or-later
 */

import {
  Codec,
  buildCodecForObject,
  buildCodecForUnion,
  codecForAny,
  codecForBoolean,
  codecForConstString,
  codecForEither,
  codecForMap,
  codecForNumber,
  codecForString,
  codecOptional,
} from "./codec.js";
import { TalerProtocolTimestamp, codecForTimestamp } from "./time.js";
import {
  Integer,
  InternationalizedString,
  Timestamp,
} from "./types-taler-common.js";

export interface ChallengerTermsOfServiceResponse {
  // Name of the service
  name: "challenger";

  // libtool-style representation of the Challenger protocol version, see
  // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
  // The format is "current:revision:age".
  version: string;

  // URN of the implementation (needed to interpret 'revision' in version).
  // @since v0, may become mandatory in the future.
  implementation?: string;

  // Object; map of keys (names of the fields of the address
  // to be entered by the user) to objects with a "regex" (string)
  // containing an extended Posix regular expression for allowed
  // address field values, and a "hint"/"hint_i18n" giving a
  // human-readable explanation to display if the value entered
  // by the user does not match the regex. Keys that are not mapped
  // to such an object have no restriction on the value provided by
  // the user.  See "ADDRESS_RESTRICTIONS" in the challenger configuration.
  restrictions: Record<string, Restriction> | undefined;

  // @since v2.
  address_type: "email" | "phone" | "postal" | "postal-ch";
}

export interface ChallengeSetupResponse {
  // Nonce to use when constructing /authorize endpoint.
  nonce: string;
}

export interface Restriction {
  regex?: string;
  hint?: string;
  hint_i18n?: InternationalizedString;
}

export interface ChallengeStatus {
  // indicates if the given address cannot be changed anymore, the
  // form should be read-only if set to true.
  fix_address: boolean;

  // form values from the previous submission if available, details depend
  // on the ADDRESS_TYPE, should be used to pre-populate the form
  last_address: Record<string, string> | undefined;

  // number of times the address can still be changed, may or may not be
  // shown to the user
  changes_left: Integer;

  // is the challenge already solved?
  solved: boolean;

  // when we would re-transmit the challenge the next
  // time (at the earliest) if requested by the user
  // only present if challenge already created
  // @since v2
  retransmission_time: Timestamp;

  // how many times might the PIN still be retransmitted
  // only present if challenge already created
  // @since v2
  pin_transmissions_left: Integer;

  // how many times might the user still try entering the PIN code
  // only present if challenge already created
  // @since v2
  auth_attempts_left: Integer;
}

export type ChallengeResponse = ChallengeRedirect | ChallengeCreateResponse;

export interface ChallengeRedirect {
  type: "completed";
  // challenge is completed, use should redirect here
  redirect_url: string;
}

export interface ChallengeCreateResponse {
  type: "created";
  // how many more attempts are allowed, might be shown to the user,
  // highlighting might be appropriate for low values such as 1 or 2 (the
  // form will never be used if the value is zero)
  attempts_left: Integer;

  // the address that is being validated, might be shown or not
  address: Object;

  // true if we just retransmitted the challenge, false if we sent a
  // challenge recently and thus refused to transmit it again this time;
  // might make a useful hint to the user
  transmitted: boolean;

  // FIXME: not in the spec
  nonce?: string;

  // timestamp explaining when we would re-transmit the challenge the next
  // time (at the earliest) if requested by the user
  retransmission_time: TalerProtocolTimestamp;
}

export type ChallengeSolveResponse = ChallengeRedirect | InvalidPinResponse;

export interface InvalidPinResponse {
  type: "pending";

  // numeric Taler error code, should be shown to indicate the error
  // compactly for reporting to developers
  ec?: number;

  // FIXME: not documented
  code?: number;

  // human-readable Taler error code, should be shown for the user to
  // understand the error
  hint: string;

  // how many times is the user still allowed to change the address;
  // if 0, the user should not be shown a link to jump to the
  // address entry form
  addresses_left: Integer;

  // how many times might the PIN still be retransmitted
  pin_transmissions_left: Integer;

  // how many times might the user still try entering the PIN code
  auth_attempts_left: Integer;

  // if true, the PIN was not even evaluated as the user previously
  // exhausted the number of attempts
  exhausted: boolean;

  // if true, the PIN was not even evaluated as no challenge was ever
  // issued (the user must have skipped the step of providing their
  // address first!)
  no_challenge: boolean;
}

export interface ChallengerAuthResponse {
  // Token used to authenticate access in /info.
  access_token: string;

  // Type of the access token.
  token_type: "Bearer";

  // Amount of time that an access token is valid (in seconds).
  expires_in: Integer;
}

export interface ChallengerInfoResponse {
  // Unique ID of the record within Challenger
  // (identifies the rowid of the token).
  id: Integer;

  // Address that was validated.
  // Key-value pairs, details depend on the
  // address_type.
  address: Object;

  // Type of the address.
  address_type: string;

  // How long do we consider the address to be
  // valid for this user.
  expires: Timestamp;
}

export const codecForChallengerTermsOfServiceResponse =
  (): Codec<ChallengerTermsOfServiceResponse> =>
    buildCodecForObject<ChallengerTermsOfServiceResponse>()
      .property("name", codecForConstString("challenger"))
      .property("version", codecForString())
      .property("implementation", codecOptional(codecForString()))
      .property("restrictions", codecOptional(codecForMap(codecForAny())))
      .property(
        "address_type",
        codecForEither(
          codecForConstString("phone"),
          codecForConstString("email"),
          codecForConstString("postal"),
          codecForConstString("postal-ch"),
        ),
      )
      .build("ChallengerApi.ChallengerTermsOfServiceResponse");

export const codecForChallengeSetupResponse =
  (): Codec<ChallengeSetupResponse> =>
    buildCodecForObject<ChallengeSetupResponse>()
      .property("nonce", codecForString())
      .build("ChallengerApi.ChallengeSetupResponse");

export const codecForChallengeStatus = (): Codec<ChallengeStatus> =>
  buildCodecForObject<ChallengeStatus>()
    .property("fix_address", codecForBoolean())
    .property("solved", codecForBoolean())
    .property("last_address", codecOptional(codecForMap(codecForAny())))
    .property("changes_left", codecForNumber())
    .property("retransmission_time", codecForTimestamp)
    .property("pin_transmissions_left", codecForNumber())
    .property("auth_attempts_left", codecForNumber())
    .build("ChallengerApi.ChallengeStatus");

export const codecForChallengeResponse = (): Codec<ChallengeResponse> =>
  buildCodecForUnion<ChallengeResponse>()
    .discriminateOn("type")
    .alternative("completed", codecForChallengeRedirect())
    .alternative("created", codecForChallengeCreateResponse())
    .build("ChallengerApi.ChallengeResponse");

export const codecForChallengeCreateResponse =
  (): Codec<ChallengeCreateResponse> =>
    buildCodecForObject<ChallengeCreateResponse>()
      .property("attempts_left", codecForNumber())
      .property("type", codecForConstString("created"))
      .property("nonce", codecOptional(codecForString()))
      .property("address", codecForAny())
      .property("transmitted", codecForBoolean())
      .property("retransmission_time", codecForTimestamp)
      .build("ChallengerApi.ChallengeCreateResponse");

export const codecForChallengeRedirect = (): Codec<ChallengeRedirect> =>
  buildCodecForObject<ChallengeRedirect>()
    .property("type", codecForConstString("completed"))
    .property("redirect_url", codecForString())
    .build("ChallengerApi.ChallengeRedirect");

export const codecForChallengeInvalidPinResponse =
  (): Codec<InvalidPinResponse> =>
    buildCodecForObject<InvalidPinResponse>()
      .property("ec", codecOptional(codecForNumber()))
      .property("code", codecOptional(codecForNumber()))
      .property("hint", codecForAny())
      .property("type", codecForConstString("pending"))
      .property("addresses_left", codecForNumber())
      .property("pin_transmissions_left", codecForNumber())
      .property("auth_attempts_left", codecForNumber())
      .property("exhausted", codecForBoolean())
      .property("no_challenge", codecForBoolean())
      .build("ChallengerApi.InvalidPinResponse");

export const codecForChallengeSolveResponse =
  (): Codec<ChallengeSolveResponse> =>
    buildCodecForUnion<ChallengeSolveResponse>()
      .discriminateOn("type")
      .alternative("completed", codecForChallengeRedirect())
      .alternative("pending", codecForChallengeInvalidPinResponse())
      .build("ChallengerApi.ChallengeSolveResponse");

export const codecForChallengerAuthResponse =
  (): Codec<ChallengerAuthResponse> =>
    buildCodecForObject<ChallengerAuthResponse>()
      .property("access_token", codecForString())
      .property("token_type", codecForAny())
      .property("expires_in", codecForNumber())
      .build("ChallengerApi.ChallengerAuthResponse");

export const codecForChallengerInfoResponse =
  (): Codec<ChallengerInfoResponse> =>
    buildCodecForObject<ChallengerInfoResponse>()
      .property("id", codecForNumber())
      .property("address", codecForAny())
      .property("address_type", codecForString())
      .property("expires", codecForTimestamp)
      .build("ChallengerApi.ChallengerInfoResponse");
