/*
 This file is part of GNU Taler
 (C) 2022-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 {
  AbsoluteTime,
  assertUnreachable,
  BitcoinBech32,
  encodeCrock,
  getURLHostnamePortPath,
  HostPortPath,
  HttpStatusCode,
  IbanString,
  OperationOk,
  parseIban,
  ParseIbanError,
  PaytoParseError,
  Paytos,
  PaytoType,
  ReservePubParseError,
  TalerError,
  TranslatedString
} from "@gnu-taler/taler-util";
import {
  Attention,
  encodeCrockForURI,
  ErrorLoading,
  FormDesign,
  FormUI,
  InternationalizationAPI,
  Loading,
  Pagination,
  RouteDefinition,
  Time,
  UIFormElementConfig,
  useExchangeApiContext,
  useForm,
  useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { HandleAccountNotReady } from "../components/HandleAccountNotReady.js";
import { useAccountDecisions } from "../hooks/decisions.js";
import { useOfficer } from "../hooks/officer.js";
import { ToInvestigateIcon } from "./AccountList.js";
import { Profile } from "./Profile.js";

const TALER_SCREEN_ID = 111;

export function Search({
  onNewDecision,
  routeToAccountById,
}: {
  onNewDecision: (account: string, payto: string) => void;
  routeToAccountById: RouteDefinition<{ cid: string }>;
}) {
  const officer = useOfficer();
  const { i18n } = useTranslationContext();

  const [paytoUri, setPayto] = useState<Paytos.URI | undefined>(undefined);

  if (officer.state !== "ready") {
    return <HandleAccountNotReady officer={officer} />;
  }

  const design: FormDesign = {
    type: "single-column",
    fields: paytoTypeField(i18n),
  };
  const paytoForm = useForm<FormPayto>(design, { paytoType: "iban" });

  return (
    <div>
      <h1 class="my-2 text-3xl font-bold tracking-tight text-gray-900 ">
        <i18n.Translate>Search account</i18n.Translate>
      </h1>
      <form
        class="space-y-6"
        noValidate
        onSubmit={(e) => {
          e.preventDefault();
        }}
        autoCapitalize="none"
        autoCorrect="off"
      >
        <FormUI design={design} model={paytoForm.model} />
      </form>

      {(function () {
        if (paytoForm.status.status !== "ok") return undefined;
        switch (paytoForm.status.result.paytoType) {
          case "iban": {
            return <IbanForm onSearch={setPayto} />;
          }
          case "generic": {
            return <GenericForm onSearch={setPayto} />;
          }
          case "x-taler-bank": {
            return <XTalerBankForm onSearch={setPayto} />;
          }
          case "wallet": {
            return <WalletForm onSearch={setPayto} />;
          }
          default: {
            assertUnreachable(paytoForm.status.result.paytoType);
          }
        }
      })()}
      {!paytoUri ? undefined : (
        <ShowResult
          payto={paytoUri}
          onNewDecision={onNewDecision}
          routeToAccountById={routeToAccountById}
        />
      )}
    </div>
  );
}

function ShowResult({
  payto,
  routeToAccountById,
  onNewDecision,
}: {
  routeToAccountById: RouteDefinition<{ cid: string }>;

  payto: Paytos.URI;
  onNewDecision: (account: string, payto: string) => void;
}): VNode {
  const paytoStr = Paytos.toNormalizedString(payto);
  const account = encodeCrock(Paytos.hash(paytoStr));
  const { i18n } = useTranslationContext();

  const history = useAccountDecisions(account);
  if (!history) {
    return <Loading />;
  }
  if (history instanceof TalerError) {
    return <ErrorLoading error={history} />;
  }
  if (history.type === "fail") {
    switch (history.case) {
      case HttpStatusCode.Forbidden:
        return (
          <Fragment>
            <Attention type="danger" title={i18n.str`Operation denied`}>
              <i18n.Translate>
                This account signature is invalid, contact administrator or
                create a new one.
              </i18n.Translate>
            </Attention>
            <Profile />
          </Fragment>
        );
      case HttpStatusCode.NotFound:
        return (
          <Fragment>
            <Attention type="danger" title={i18n.str`Operation denied`}>
              <i18n.Translate>
                The designated AML account is not known, contact administrator
                or create a new one.
              </i18n.Translate>
            </Attention>
            <Profile />
          </Fragment>
        );
      case HttpStatusCode.Conflict:
        return (
          <Fragment>
            <Attention type="danger" title={i18n.str`Operation denied`}>
              <i18n.Translate>
                The designated AML account is not enabled, contact administrator
                or create a new one.
              </i18n.Translate>
            </Attention>
            <Profile />
          </Fragment>
        );
      default:
        assertUnreachable(history);
    }
  }

  if (history.body.length) {
    return (
      <div class="mt-8">
        <div class="mb-2">
          <a
            href={routeToAccountById.url({
              cid: account,
            })}
            class="text-indigo-600 hover:text-indigo-900"
          >
            <i18n.Translate>Check account details</i18n.Translate>
          </a>
        </div>
        <div class="sm:flex sm:items-center">
          <div class="sm:flex-auto">
            <div>
              <h1 class="text-base font-semibold leading-6 text-gray-900">
                <i18n.Translate>Account most recent decisions</i18n.Translate>
              </h1>
            </div>
          </div>
        </div>

        <div class="flow-root">
          <div class="overflow-x-auto">
            {!history.body.length ? (
              <div>empty result </div>
            ) : (
              <div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
                <table class="min-w-full divide-y divide-gray-300">
                  <thead>
                    <tr>
                      <th
                        scope="col"
                        class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 w-80"
                      >
                        <i18n.Translate>When</i18n.Translate>
                      </th>
                      <th
                        scope="col"
                        class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 w-80"
                      >
                        <i18n.Translate>Justification</i18n.Translate>
                      </th>
                      <th
                        scope="col"
                        class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 w-40"
                      >
                        <i18n.Translate>Status</i18n.Translate>
                      </th>
                    </tr>
                  </thead>
                  <tbody class="divide-y divide-gray-200 bg-white">
                    {history.body.map((r, idx) => {
                      return (
                        <tr key={r.h_payto} class="hover:bg-gray-100 ">
                          <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500 ">
                            <Time
                              format="dd/MM/yyyy HH:mm"
                              timestamp={AbsoluteTime.fromProtocolTimestamp(
                                r.decision_time,
                              )}
                            />
                          </td>
                          <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500 ">
                            {r.justification}
                          </td>
                          <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-900">
                            {idx === 0 ? (
                              <span class="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10">
                                <i18n.Translate>LATEST</i18n.Translate>
                              </span>
                            ) : undefined}
                            {r.is_active ? (
                              <span class="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10">
                                <i18n.Translate>ACTIVE</i18n.Translate>
                              </span>
                            ) : undefined}
                            {r.decision_time ? (
                              <span title="require investigation">
                                <ToInvestigateIcon />
                              </span>
                            ) : undefined}
                          </td>
                        </tr>
                      );
                    })}
                  </tbody>
                </table>
                <Pagination
                  onFirstPage={history.loadFirst}
                  onNext={history.loadNext}
                />
              </div>
            )}
          </div>
        </div>
      </div>
    );
  }
  return (
    <div class="mt-4">
      <Attention title={i18n.str`Account not found`} type="warning">
        <i18n.Translate>
          There is no history known for this account yet.
        </i18n.Translate>
        &nbsp;
        <button
          onClick={async () => {
            onNewDecision(account, encodeCrockForURI(paytoStr));
          }}
          class="text-indigo-600 hover:text-indigo-900"
        >
          <i18n.Translate>
            You can make a decision for this account anyway.
          </i18n.Translate>
        </button>
      </Attention>
    </div>
  );
}

function XTalerBankForm({
  onSearch,
}: {
  onSearch: (p: Paytos.URI | undefined) => void;
}): VNode {
  const { i18n } = useTranslationContext();
  const design: FormDesign = {
    type: "single-column",
    fields: talerBankFields(i18n),
  };
  const form = useForm<PaytoUriTalerBankForm>(
    design,
    {},
    // createTalerBankPaytoValidator(i18n),
  );
  const paytoUri =
    form.status.status === "fail"
      ? undefined
      : Paytos.createTalerBank(
          form.status.result.hostname,
          form.status.result.account,
        );

  return (
    <form
      class="space-y-6"
      noValidate
      onSubmit={(e) => {
        e.preventDefault();
      }}
      autoCapitalize="none"
      autoCorrect="off"
    >
      <FormUI design={design} model={form.model} />

      <button
        disabled={form.status.status === "fail"}
        class="disabled:bg-gray-100 disabled:text-gray-500 m-4  rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
        onClick={() => onSearch(paytoUri)}
      >
        <i18n.Translate>Search</i18n.Translate>
      </button>
    </form>
  );
}

function IbanForm({
  onSearch,
}: {
  onSearch: (p: Paytos.URI | undefined) => void;
}): VNode {
  const { i18n } = useTranslationContext();
  const design: FormDesign = {
    type: "single-column",
    fields: ibanFields(i18n),
  };
  const form = useForm<PaytoUriIBANForm>(design, {});
  const paytoUri =
    form.status.status === "fail"
      ? undefined
      : Paytos.createIban(
          form.status.result.account as IbanString,
          undefined,
          {},
        );

  return (
    <form
      class="space-y-6"
      noValidate
      onSubmit={(e) => {
        e.preventDefault();
      }}
      autoCapitalize="none"
      autoCorrect="off"
    >
      <FormUI design={design} model={form.model} />

      <button
        disabled={form.status.status === "fail"}
        class="disabled:bg-gray-100 disabled:text-gray-500 m-4  rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
        onClick={() => onSearch(paytoUri)}
      >
        <i18n.Translate>Search</i18n.Translate>
      </button>
    </form>
  );
}
function WalletForm({
  onSearch,
}: {
  onSearch: (p: Paytos.URI | undefined) => void;
}): VNode {
  const { i18n } = useTranslationContext();
  const { config } = useExchangeApiContext();
  const design: FormDesign = {
    type: "single-column",
    fields: walletFields(i18n),
  };
  const form = useForm<PaytoUriTalerForm>(
    design,
    {
      exchange: getURLHostnamePortPath(config.keys.base_url),
    },
    // createTalerPaytoValidator(i18n),
  );
  const pub = Paytos.parseReservePub(form.status.result.reservePub);

  const paytoUri =
    form.status.status === "fail" || pub.type === "fail"
      ? undefined
      : Paytos.createTalerReserve(form.status.result.exchange, pub.body);

  return (
    <form
      class="space-y-6"
      noValidate
      onSubmit={(e) => {
        e.preventDefault();
      }}
      autoCapitalize="none"
      autoCorrect="off"
    >
      <FormUI design={design} model={form.model} />

      <button
        disabled={form.status.status === "fail"}
        class="disabled:bg-gray-100 disabled:text-gray-500 m-4  rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
        onClick={() => onSearch(paytoUri)}
      >
        <i18n.Translate>Search</i18n.Translate>
      </button>
    </form>
  );
}

function GenericForm({
  onSearch,
}: {
  onSearch: (p: Paytos.URI | undefined) => void;
}): VNode {
  const { i18n } = useTranslationContext();
  const design: FormDesign = {
    type: "single-column",
    fields: genericFields(i18n),
  };
  const form = useForm<PaytoUriGenericForm>(
    design,
    {},
    // createGenericPaytoValidator(i18n),
  );
  let p;
  const paytoUri =
    form.status.status === "fail"
      ? undefined
      : (p = Paytos.fromString(form.status.result.payto)).type === "fail"
        ? undefined
        : p.body;
  return (
    <form
      class="space-y-6"
      noValidate
      onSubmit={(e) => {
        e.preventDefault();
      }}
      autoCapitalize="none"
      autoCorrect="off"
    >
      <FormUI design={design} model={form.model} />
      <button
        disabled={form.status.status === "fail"}
        class="disabled:bg-gray-100 disabled:text-gray-500 m-4  rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700"
        onClick={() => onSearch(paytoUri)}
      >
        <i18n.Translate>Search</i18n.Translate>
      </button>
    </form>
  );
}

interface FormPayto {
  paytoType: "generic" | "iban" | "x-taler-bank" | "wallet";
}

interface PaytoUriGenericForm {
  payto: string;
}

interface PaytoUriIBANForm {
  account: string;
}

interface PaytoUriTalerBankForm {
  hostname: HostPortPath;
  account: string;
}

interface PaytoUriTalerForm {
  exchange: HostPortPath;
  reservePub: string;
}

const paytoTypeField: (
  i18n: InternationalizationAPI,
) => UIFormElementConfig[] = (i18n) => [
  {
    id: "paytoType",
    type: "choiceHorizontal",
    required: true,
    choices: [
      {
        value: "iban",
        label: i18n.str`IBAN`,
      },
      {
        value: "x-taler-bank",
        label: i18n.str`Taler Bank`,
      },
      {
        value: "wallet",
        label: i18n.str`Wallet`,
      },
      {
        value: "generic",
        label: i18n.str`Generic Payto:// URI`,
      },
    ],
    label: i18n.str`Account type`,
  },
];

type FailCasesOf<T extends (...args: any) => any> = Exclude<
  ReturnType<T>,
  OperationOk<any>
>;
function translateReservePubError(
  result: FailCasesOf<typeof Paytos.parseReservePub>,
  i18n: InternationalizationAPI,
): TranslatedString {
  switch (result.case) {
    case ReservePubParseError.WRONG_LENGTH:
      return i18n.str`Should be 52 characters.`;
    case ReservePubParseError.DECODE_ERROR:
      return i18n.str`Failed to parse: ${result.body.message}`;
  }
}

function translateBitcoinError(
  result: FailCasesOf<typeof BitcoinBech32.decode>,
  i18n: InternationalizationAPI,
): TranslatedString {
  switch (result.case) {
    case BitcoinBech32.BitcoinParseError.WRONG_CHARSET:
      return i18n.str`Address contains invalid characters.`;
    case BitcoinBech32.BitcoinParseError.MIXING_UPPER_AND_LOWER:
      return i18n.str`Can't mix uppercased and lowercased characters.`;
    case BitcoinBech32.BitcoinParseError.WRONG_CHECKSUM:
      return i18n.str`The addr checksum doesn't match.`;
    case BitcoinBech32.BitcoinParseError.MISSING_HRP:
      return i18n.str`Missing separator or address type.`;
    case BitcoinBech32.BitcoinParseError.TOO_LONG:
      return i18n.str`Address should have less than 90 characters.`;
    case BitcoinBech32.BitcoinParseError.TOO_SHORT:
      return i18n.str`Address should have more than 6 characters.`;
  }
}

function translateIbanError(
  result: FailCasesOf<typeof parseIban>,
  i18n: InternationalizationAPI,
): TranslatedString {
  switch (result.case) {
    case ParseIbanError.UNSUPPORTED_COUNTRY:
      return i18n.str`Unsupported country.`;
    case ParseIbanError.TOO_LONG:
      return i18n.str`IBANs have fewer than 34 characters.`;
    case ParseIbanError.TOO_SHORT:
      return i18n.str`IBANs have more than 4 characters.`;
    case ParseIbanError.INVALID_CHARSET:
      return i18n.str`It should only contain numbers and letters.`;
    case ParseIbanError.INVALID_CHECKSUM:
      return i18n.str`The checksum is wrong.`;
  }
}

function translatePaytoError(
  result: FailCasesOf<typeof Paytos.fromString>,
  i18n: InternationalizationAPI,
): TranslatedString {
  switch (result.case) {
    case PaytoParseError.WRONG_PREFIX:
      return i18n.str`The string should start with payto://`;
    case PaytoParseError.INCOMPLETE:
      return i18n.str`After the target type it should follow a '/' with more information.`;
    case PaytoParseError.UNSUPPORTED:
      return i18n.str`The target type is not supported, only x-taler-bank, taler-reserve or iban`;
    case PaytoParseError.COMPONENTS_LENGTH: {
      switch (result.body.targetType) {
        case PaytoType.IBAN:
          return i18n.str`IBAN is missing`;
        case PaytoType.Bitcoin:
          return i18n.str`Bitcoin address is missing`;
        case PaytoType.TalerBank:
          return i18n.str`Bank host and account is missing`;
        case PaytoType.TalerReserve:
          return i18n.str`Exchange host and account is missing`;
        case PaytoType.TalerReserveHttp:
          return i18n.str`Exchange host and account is missing`;
        case PaytoType.Ethereum:
          return i18n.str`Ethereum address is missing`;
      }
    }
    case PaytoParseError.INVALID_TARGET_PATH: {
      switch (result.body.targetType) {
        case PaytoType.IBAN:
          return i18n.str`Invalid IBAN: ${translateIbanError(
            result.body.error,
            i18n,
          )}`;
        case PaytoType.Bitcoin: {
          switch (result.body.pos) {
            case 0:
              return i18n.str`Invalid BTC: ${translateBitcoinError(
                result.body.error,
                i18n,
              )}`;
            case 1:
              return i18n.str`Invalid reserve: ${translateReservePubError(
                result.body.error,
                i18n,
              )}`;
            default:
              assertUnreachable(result.body);
          }
        }
        case PaytoType.TalerBank: {
          switch (result.body.pos) {
            case 0:
              return i18n.str`Invalid host`;
            case 1:
              return i18n.str`Invalid account`;
            default:
              assertUnreachable(result.body);
          }
        }
        case PaytoType.TalerReserve: {
          switch (result.body.pos) {
            case 0:
              return i18n.str`Invalid host`;
            case 1:
              return i18n.str`Invalid reserve: ${translateReservePubError(
                result.body.error,
                i18n,
              )}`;
            default:
              assertUnreachable(result.body);
          }
        }
        case PaytoType.TalerReserveHttp: {
          switch (result.body.pos) {
            case 0:
              return i18n.str`Invalid host`;
            case 1:
              return i18n.str`Invalid reserve: ${translateReservePubError(
                result.body.error,
                i18n,
              )}`;
            default:
              assertUnreachable(result.body);
          }
        }
        case PaytoType.Ethereum: {
          switch (result.body.pos) {
            case 0:
              return i18n.str`Invalid address`;
            default:
              assertUnreachable(result.body);
          }
        }
      }
    }
  }
}

function validatePayto(s: string, i18n: InternationalizationAPI) {
  const result = Paytos.fromString(s);
  if (result.type === "ok") return undefined;
  return translatePaytoError(result, i18n);
}

const genericFields: (
  i18n: InternationalizationAPI,
) => UIFormElementConfig[] = (i18n) => [
  {
    id: "payto",
    type: "textArea",
    required: true,
    label: i18n.str`Payto URI`,
    help: i18n.str`As defined by RFC 8905`,
    placeholder: i18n.str`payto://`,
    validator(value) {
      return !value ? i18n.str`Required` : validatePayto(value, i18n);
    },
  },
];
const ibanFields: (i18n: InternationalizationAPI) => UIFormElementConfig[] = (
  i18n,
) => [
  {
    id: "account",
    type: "text",
    required: true,
    label: i18n.str`IBAN`,
    help: i18n.str`International Bank Account Number`,
    placeholder: i18n.str`DE1231231231`,
    validator: (value) => validateIBAN(value, i18n),
  },
];

const walletFields: (i18n: InternationalizationAPI) => UIFormElementConfig[] = (
  i18n,
) => [
  {
    id: "exchange",
    type: "text",
    required: true,
    label: i18n.str`Host`,
    help: i18n.str`Exchange hostname`,
    placeholder: i18n.str`exchange.taler.net`,
    validator(value) {
      return !value
        ? i18n.str`Required`
        : value.endsWith("/")
          ? i18n.str`Remove last '/'`
          : !Paytos.parseHostPortPath(value)
            ? i18n.str`Invalid value`
            : undefined;
    },
  },
  {
    id: "reservePub",
    type: "text",
    required: true,
    label: i18n.str`ID`,
    help: i18n.str`Wallet reserve public key`,
    placeholder: i18n.str`abcdef1235`,
    validator(value) {
      return !value
        ? i18n.str`Required`
        : value.length !== 52
          ? i18n.str`Should be 52 characters`
          : Paytos.parseReservePub(value).type === "fail"
            ? i18n.str`Invalid value`
            : undefined;
    },
  },
];

const talerBankFields: (
  i18n: InternationalizationAPI,
) => UIFormElementConfig[] = (i18n) => [
  {
    id: "account",
    type: "text",
    required: true,
    label: i18n.str`Account`,
    help: i18n.str`Bank account identification`,
    placeholder: i18n.str`DE123123123`,
  },
  {
    id: "hostname",
    type: "text",
    required: true,
    label: i18n.str`Hostname`,
    help: i18n.str`Without the scheme, may include subpath: bank.com, bank.com/path/`,
    placeholder: i18n.str`bank.demo.taler.net`,
    validator: (value) =>
      !value
        ? i18n.str`Required`
        : value.endsWith("/")
          ? i18n.str`Remove last '/'`
          : !Paytos.parseHostPortPath(value)
            ? i18n.str`Invalid hostname`
            : undefined,
  },
];

function validateIBAN(
  iban: string,
  i18n: ReturnType<typeof useTranslationContext>["i18n"],
): TranslatedString | undefined {
  if (!iban) {
    return i18n.str`Required`;
  }
  const result = parseIban(iban);
  if (result.type === "ok") {
    return undefined;
  }
  return translateIbanError(result, i18n);
}
