/*
 This file is part of GNU Taler
 (C) 2022 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 {
  Amounts,
  parsePaytoUri,
  TranslatedString,
} from "@gnu-taler/taler-util";
import {
  HttpResponsePaginated,
  RequestError,
  useTranslationContext,
} from "@gnu-taler/web-util/lib/index.browser";
import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { Cashouts } from "../components/Cashouts/index.js";
import { ErrorMessage, usePageContext } from "../context/pageState.js";
import { useAccountDetails } from "../hooks/access.js";
import {
  useBusinessAccountDetails,
  useBusinessAccounts,
  useAdminAccountAPI,
} from "../hooks/circuit.js";
import {
  PartialButDefined,
  undefinedIfEmpty,
  WithIntermediate,
} from "../utils.js";
import { ErrorBanner } from "./BankFrame.js";
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";

const charset =
  "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const upperIdx = charset.indexOf("A");

function randomPassword(): string {
  const random = Array.from({ length: 16 }).map(() => {
    return charset.charCodeAt(Math.random() * charset.length);
  });
  // first char can't be upper
  const charIdx = charset.indexOf(String.fromCharCode(random[0]));
  random[0] =
    charIdx > upperIdx ? charset.charCodeAt(charIdx - upperIdx) : random[0];
  return String.fromCharCode(...random);
}

interface Props {
  onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode;
}
/**
 * Query account information and show QR code if there is pending withdrawal
 */
export function AdminPage({ onLoadNotOk }: Props): VNode {
  const [account, setAccount] = useState<string | undefined>();
  const [showDetails, setShowDetails] = useState<string | undefined>();
  const [showCashouts, setShowCashouts] = useState<string | undefined>();
  const [updatePassword, setUpdatePassword] = useState<string | undefined>();
  const [removeAccount, setRemoveAccount] = useState<string | undefined>();

  const [createAccount, setCreateAccount] = useState(false);
  const { pageStateSetter } = usePageContext();

  function showInfoMessage(info: TranslatedString): void {
    pageStateSetter((prev) => ({
      ...prev,
      info,
    }));
  }

  const result = useBusinessAccounts({ account });
  const { i18n } = useTranslationContext();

  if (result.loading) return <div />;
  if (!result.ok) {
    return onLoadNotOk(result);
  }

  const { customers } = result.data;

  if (showCashouts) {
    return (
      <div>
        <Cashouts account={showCashouts} />
        <input
          class="pure-button"
          type="submit"
          value={i18n.str`Close`}
          onClick={async (e) => {
            e.preventDefault();
            setShowCashouts(undefined);
          }}
        />
      </div>
    );
  }

  if (showDetails) {
    return (
      <ShowAccountDetails
        account={showDetails}
        onLoadNotOk={onLoadNotOk}
        onChangePassword={() => {
          setUpdatePassword(showDetails);
          setShowDetails(undefined);
        }}
        onUpdateSuccess={() => {
          showInfoMessage(i18n.str`Account updated`);
          setShowDetails(undefined);
        }}
        onClear={() => {
          setShowDetails(undefined);
        }}
      />
    );
  }
  if (removeAccount) {
    return (
      <RemoveAccount
        account={removeAccount}
        onLoadNotOk={onLoadNotOk}
        onUpdateSuccess={() => {
          showInfoMessage(i18n.str`Account removed`);
          setRemoveAccount(undefined);
        }}
        onClear={() => {
          setRemoveAccount(undefined);
        }}
      />
    );
  }
  if (updatePassword) {
    return (
      <UpdateAccountPassword
        account={updatePassword}
        onLoadNotOk={onLoadNotOk}
        onUpdateSuccess={() => {
          showInfoMessage(i18n.str`Password changed`);
          setUpdatePassword(undefined);
        }}
        onClear={() => {
          setUpdatePassword(undefined);
        }}
      />
    );
  }
  if (createAccount) {
    return (
      <CreateNewAccount
        onClose={() => setCreateAccount(false)}
        onCreateSuccess={(password) => {
          showInfoMessage(
            i18n.str`Account created with password "${password}"`,
          );
          setCreateAccount(false);
        }}
      />
    );
  }
  return (
    <Fragment>
      <div>
        <h1 class="nav welcome-text">
          <i18n.Translate>Admin panel</i18n.Translate>
        </h1>
      </div>

      <p>
        <div style={{ display: "flex", justifyContent: "space-between" }}>
          <div></div>
          <div>
            <input
              class="pure-button pure-button-primary content"
              type="submit"
              value={i18n.str`Create account`}
              onClick={async (e) => {
                e.preventDefault();

                setCreateAccount(true);
              }}
            />
          </div>
        </div>
      </p>

      <section id="main">
        <article>
          <h2>{i18n.str`Accounts:`}</h2>
          <div class="results">
            <table class="pure-table pure-table-striped">
              <thead>
                <tr>
                  <th>{i18n.str`Username`}</th>
                  <th>{i18n.str`Name`}</th>
                  <th></th>
                  <th></th>
                </tr>
              </thead>
              <tbody>
                {customers.map((item, idx) => {
                  return (
                    <tr key={idx}>
                      <td>
                        <a
                          href="#"
                          onClick={(e) => {
                            e.preventDefault();
                            setShowDetails(item.username);
                          }}
                        >
                          {item.username}
                        </a>
                      </td>
                      <td>{item.name}</td>
                      <td>
                        <a
                          href="#"
                          onClick={(e) => {
                            e.preventDefault();
                            setUpdatePassword(item.username);
                          }}
                        >
                          change password
                        </a>
                      </td>
                      <td>
                        <a
                          href="#"
                          onClick={(e) => {
                            e.preventDefault();
                            setShowCashouts(item.username);
                          }}
                        >
                          cashouts
                        </a>
                      </td>
                      <td>
                        <a
                          href="#"
                          onClick={(e) => {
                            e.preventDefault();
                            setRemoveAccount(item.username);
                          }}
                        >
                          remove
                        </a>
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
        </article>
      </section>
    </Fragment>
  );
}

const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/;
const EMAIL_REGEX =
  /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
const REGEX_JUST_NUMBERS_REGEX = /^\+[0-9 ]*$/;

function initializeFromTemplate(
  account: SandboxBackend.Circuit.CircuitAccountData | undefined,
): WithIntermediate<SandboxBackend.Circuit.CircuitAccountData> {
  const emptyAccount = {
    cashout_address: undefined,
    iban: undefined,
    name: undefined,
    username: undefined,
    contact_data: undefined,
  };
  const emptyContact = {
    email: undefined,
    phone: undefined,
  };

  const initial: PartialButDefined<SandboxBackend.Circuit.CircuitAccountData> =
    structuredClone(account) ?? emptyAccount;
  if (typeof initial.contact_data === "undefined") {
    initial.contact_data = emptyContact;
  }
  initial.contact_data.email;
  return initial as any;
}

export function UpdateAccountPassword({
  account,
  onClear,
  onUpdateSuccess,
  onLoadNotOk,
}: {
  onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode;
  onClear: () => void;
  onUpdateSuccess: () => void;
  account: string;
}): VNode {
  const { i18n } = useTranslationContext();
  const result = useBusinessAccountDetails(account);
  const { changePassword } = useAdminAccountAPI();
  const [password, setPassword] = useState<string | undefined>();
  const [repeat, setRepeat] = useState<string | undefined>();
  const [error, saveError] = useState<ErrorMessage | undefined>();

  if (result.clientError) {
    if (result.isNotfound) return <div>account not found</div>;
  }
  if (!result.ok) {
    return onLoadNotOk(result);
  }

  const errors = undefinedIfEmpty({
    password: !password ? i18n.str`required` : undefined,
    repeat: !repeat
      ? i18n.str`required`
      : password !== repeat
      ? i18n.str`password doesn't match`
      : undefined,
  });

  return (
    <div>
      <div>
        <h1 class="nav welcome-text">
          <i18n.Translate>Update password for {account}</i18n.Translate>
        </h1>
      </div>
      {error && (
        <ErrorBanner error={error} onClear={() => saveError(undefined)} />
      )}

      <form class="pure-form">
        <fieldset>
          <label>{i18n.str`Password`}</label>
          <input
            type="password"
            value={password ?? ""}
            onChange={(e) => {
              setPassword(e.currentTarget.value);
            }}
          />
          <ShowInputErrorLabel
            message={errors?.password}
            isDirty={password !== undefined}
          />
        </fieldset>
        <fieldset>
          <label>{i18n.str`Repeast password`}</label>
          <input
            type="password"
            value={repeat ?? ""}
            onChange={(e) => {
              setRepeat(e.currentTarget.value);
            }}
          />
          <ShowInputErrorLabel
            message={errors?.repeat}
            isDirty={repeat !== undefined}
          />
        </fieldset>
      </form>
      <p>
        <div style={{ display: "flex", justifyContent: "space-between" }}>
          <div>
            <input
              class="pure-button"
              type="submit"
              value={i18n.str`Close`}
              onClick={async (e) => {
                e.preventDefault();
                onClear();
              }}
            />
          </div>
          <div>
            <input
              id="select-exchange"
              class="pure-button pure-button-primary content"
              disabled={!!errors}
              type="submit"
              value={i18n.str`Confirm`}
              onClick={async (e) => {
                e.preventDefault();
                if (!!errors || !password) return;
                try {
                  const r = await changePassword(account, {
                    new_password: password,
                  });
                  onUpdateSuccess();
                } catch (error) {
                  handleError(error, saveError, i18n);
                }
              }}
            />
          </div>
        </div>
      </p>
    </div>
  );
}

function CreateNewAccount({
  onClose,
  onCreateSuccess,
}: {
  onClose: () => void;
  onCreateSuccess: (password: string) => void;
}): VNode {
  const { i18n } = useTranslationContext();
  const { createAccount } = useAdminAccountAPI();
  const [submitAccount, setSubmitAccount] = useState<
    SandboxBackend.Circuit.CircuitAccountData | undefined
  >();
  const [error, saveError] = useState<ErrorMessage | undefined>();
  return (
    <div>
      <div>
        <h1 class="nav welcome-text">
          <i18n.Translate>New account</i18n.Translate>
        </h1>
      </div>
      {error && (
        <ErrorBanner error={error} onClear={() => saveError(undefined)} />
      )}

      <AccountForm
        template={undefined}
        purpose="create"
        onChange={(a) => setSubmitAccount(a)}
      />

      <p>
        <div style={{ display: "flex", justifyContent: "space-between" }}>
          <div>
            <input
              class="pure-button"
              type="submit"
              value={i18n.str`Close`}
              onClick={async (e) => {
                e.preventDefault();
                onClose();
              }}
            />
          </div>
          <div>
            <input
              id="select-exchange"
              class="pure-button pure-button-primary content"
              disabled={!submitAccount}
              type="submit"
              value={i18n.str`Confirm`}
              onClick={async (e) => {
                e.preventDefault();

                if (!submitAccount) return;
                try {
                  const account: SandboxBackend.Circuit.CircuitAccountRequest =
                    {
                      cashout_address: submitAccount.cashout_address,
                      contact_data: submitAccount.contact_data,
                      internal_iban: submitAccount.iban,
                      name: submitAccount.name,
                      username: submitAccount.username,
                      password: randomPassword(),
                    };

                  await createAccount(account);
                  onCreateSuccess(account.password);
                } catch (error) {
                  handleError(error, saveError, i18n);
                }
              }}
            />
          </div>
        </div>
      </p>
    </div>
  );
}

export function ShowAccountDetails({
  account,
  onClear,
  onUpdateSuccess,
  onLoadNotOk,
  onChangePassword,
}: {
  onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode;
  onClear?: () => void;
  onChangePassword: () => void;
  onUpdateSuccess: () => void;
  account: string;
}): VNode {
  const { i18n } = useTranslationContext();
  const result = useBusinessAccountDetails(account);
  const { updateAccount } = useAdminAccountAPI();
  const [update, setUpdate] = useState(false);
  const [submitAccount, setSubmitAccount] = useState<
    SandboxBackend.Circuit.CircuitAccountData | undefined
  >();
  const [error, saveError] = useState<ErrorMessage | undefined>();

  if (result.clientError) {
    if (result.isNotfound) return <div>account not found</div>;
  }
  if (!result.ok) {
    return onLoadNotOk(result);
  }

  return (
    <div>
      <div>
        <h1 class="nav welcome-text">
          <i18n.Translate>Business account details</i18n.Translate>
        </h1>
      </div>
      {error && (
        <ErrorBanner error={error} onClear={() => saveError(undefined)} />
      )}
      <AccountForm
        template={result.data}
        purpose={update ? "update" : "show"}
        onChange={(a) => setSubmitAccount(a)}
      />

      <p>
        <div style={{ display: "flex", justifyContent: "space-between" }}>
          <div>
            {onClear ? (
              <input
                class="pure-button"
                type="submit"
                value={i18n.str`Close`}
                onClick={async (e) => {
                  e.preventDefault();
                  onClear();
                }}
              />
            ) : undefined}
          </div>
          <div style={{ display: "flex" }}>
            <div>
              <input
                id="select-exchange"
                class="pure-button pure-button-primary content"
                disabled={update && !submitAccount}
                type="submit"
                value={i18n.str`Change password`}
                onClick={async (e) => {
                  e.preventDefault();
                  onChangePassword();
                }}
              />
            </div>
            <div>
              <input
                id="select-exchange"
                class="pure-button pure-button-primary content"
                disabled={update && !submitAccount}
                type="submit"
                value={update ? i18n.str`Confirm` : i18n.str`Update`}
                onClick={async (e) => {
                  e.preventDefault();

                  if (!update) {
                    setUpdate(true);
                  } else {
                    if (!submitAccount) return;
                    try {
                      await updateAccount(account, {
                        cashout_address: submitAccount.cashout_address,
                        contact_data: submitAccount.contact_data,
                      });
                      onUpdateSuccess();
                    } catch (error) {
                      handleError(error, saveError, i18n);
                    }
                  }
                }}
              />
            </div>
          </div>
        </div>
      </p>
    </div>
  );
}

function RemoveAccount({
  account,
  onClear,
  onUpdateSuccess,
  onLoadNotOk,
}: {
  onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode;
  onClear: () => void;
  onUpdateSuccess: () => void;
  account: string;
}): VNode {
  const { i18n } = useTranslationContext();
  const result = useAccountDetails(account);
  const { deleteAccount } = useAdminAccountAPI();
  const [error, saveError] = useState<ErrorMessage | undefined>();

  if (result.clientError) {
    if (result.isNotfound) return <div>account not found</div>;
  }
  if (!result.ok) {
    return onLoadNotOk(result);
  }

  const balance = Amounts.parse(result.data.balance.amount);
  if (!balance) {
    return <div>there was an error reading the balance</div>;
  }
  const isBalanceEmpty = Amounts.isZero(balance);
  return (
    <div>
      <div>
        <h1 class="nav welcome-text">
          <i18n.Translate>Remove account: {account}</i18n.Translate>
        </h1>
      </div>
      {!isBalanceEmpty && (
        <ErrorBanner
          error={{
            title: i18n.str`Can't delete the account`,
            description: i18n.str`Balance is not empty`,
          }}
        />
      )}
      {error && (
        <ErrorBanner error={error} onClear={() => saveError(undefined)} />
      )}

      <p>
        <div style={{ display: "flex", justifyContent: "space-between" }}>
          <div>
            <input
              class="pure-button"
              type="submit"
              value={i18n.str`Cancel`}
              onClick={async (e) => {
                e.preventDefault();
                onClear();
              }}
            />
          </div>
          <div>
            <input
              id="select-exchange"
              class="pure-button pure-button-primary content"
              disabled={!isBalanceEmpty}
              type="submit"
              value={i18n.str`Confirm`}
              onClick={async (e) => {
                e.preventDefault();
                try {
                  const r = await deleteAccount(account);
                  onUpdateSuccess();
                } catch (error) {
                  handleError(error, saveError, i18n);
                }
              }}
            />
          </div>
        </div>
      </p>
    </div>
  );
}

/**
 * Create valid account object to update or create
 * Take template as initial values for the form
 * Purpose indicate if all field al read only (show), part of them (update)
 * or none (create)
 * @param param0
 * @returns
 */
function AccountForm({
  template,
  purpose,
  onChange,
}: {
  template: SandboxBackend.Circuit.CircuitAccountData | undefined;
  onChange: (a: SandboxBackend.Circuit.CircuitAccountData | undefined) => void;
  purpose: "create" | "update" | "show";
}): VNode {
  const initial = initializeFromTemplate(template);
  const [form, setForm] = useState(initial);
  const [errors, setErrors] = useState<typeof initial | undefined>(undefined);
  const { i18n } = useTranslationContext();

  function updateForm(newForm: typeof initial): void {
    const parsed = !newForm.cashout_address
      ? undefined
      : parsePaytoUri(newForm.cashout_address);

    const validationResult = undefinedIfEmpty<typeof initial>({
      cashout_address: !newForm.cashout_address
        ? i18n.str`required`
        : !parsed
        ? i18n.str`does not follow the pattern`
        : !parsed.isKnown || parsed.targetType !== "iban"
        ? i18n.str`only "IBAN" target are supported`
        : !IBAN_REGEX.test(parsed.iban)
        ? i18n.str`IBAN should have just uppercased letters and numbers`
        : undefined,
      contact_data: {
        email: !newForm.contact_data.email
          ? undefined
          : !EMAIL_REGEX.test(newForm.contact_data.email)
          ? i18n.str`it should be an email`
          : undefined,
        phone: !newForm.contact_data.phone
          ? undefined
          : !newForm.contact_data.phone.startsWith("+")
          ? i18n.str`should start with +`
          : !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone)
          ? i18n.str`phone number can't have other than numbers`
          : undefined,
      },
      iban: !newForm.iban
        ? i18n.str`required`
        : !IBAN_REGEX.test(newForm.iban)
        ? i18n.str`IBAN should have just uppercased letters and numbers`
        : undefined,
      name: !newForm.name ? i18n.str`required` : undefined,
      username: !newForm.username ? i18n.str`required` : undefined,
    });

    setErrors(validationResult);
    setForm(newForm);
    onChange(validationResult === undefined ? undefined : (newForm as any));
  }

  return (
    <form class="pure-form">
      <fieldset>
        <label for="username">{i18n.str`Username`}</label>
        <input
          name="username"
          type="text"
          disabled={purpose !== "create"}
          value={form.username}
          onChange={(e) => {
            form.username = e.currentTarget.value;
            updateForm(structuredClone(form));
          }}
        />
        <ShowInputErrorLabel
          message={errors?.username}
          isDirty={form.username !== undefined}
        />
      </fieldset>
      <fieldset>
        <label>{i18n.str`Name`}</label>
        <input
          disabled={purpose !== "create"}
          value={form.name ?? ""}
          onChange={(e) => {
            form.name = e.currentTarget.value;
            updateForm(structuredClone(form));
          }}
        />
        <ShowInputErrorLabel
          message={errors?.name}
          isDirty={form.name !== undefined}
        />
      </fieldset>
      <fieldset>
        <label>{i18n.str`IBAN`}</label>
        <input
          disabled={purpose !== "create"}
          value={form.iban ?? ""}
          onChange={(e) => {
            form.iban = e.currentTarget.value;
            updateForm(structuredClone(form));
          }}
        />
        <ShowInputErrorLabel
          message={errors?.iban}
          isDirty={form.iban !== undefined}
        />
      </fieldset>
      <fieldset>
        <label>{i18n.str`Email`}</label>
        <input
          disabled={purpose === "show"}
          value={form.contact_data.email ?? ""}
          onChange={(e) => {
            form.contact_data.email = e.currentTarget.value;
            updateForm(structuredClone(form));
          }}
        />
        <ShowInputErrorLabel
          message={errors?.contact_data.email}
          isDirty={form.contact_data.email !== undefined}
        />
      </fieldset>
      <fieldset>
        <label>{i18n.str`Phone`}</label>
        <input
          disabled={purpose === "show"}
          value={form.contact_data.phone ?? ""}
          onChange={(e) => {
            form.contact_data.phone = e.currentTarget.value;
            updateForm(structuredClone(form));
          }}
        />
        <ShowInputErrorLabel
          message={errors?.contact_data.phone}
          isDirty={form.contact_data?.phone !== undefined}
        />
      </fieldset>
      <fieldset>
        <label>{i18n.str`Cashout address`}</label>
        <input
          disabled={purpose === "show"}
          value={form.cashout_address ?? ""}
          onChange={(e) => {
            form.cashout_address = e.currentTarget.value;
            updateForm(structuredClone(form));
          }}
        />
        <ShowInputErrorLabel
          message={errors?.cashout_address}
          isDirty={form.cashout_address !== undefined}
        />
      </fieldset>
    </form>
  );
}

function handleError(
  error: unknown,
  saveError: (e: ErrorMessage) => void,
  i18n: ReturnType<typeof useTranslationContext>["i18n"],
): void {
  if (error instanceof RequestError) {
    const payload = error.info.error as SandboxBackend.SandboxError;
    saveError({
      title: error.info.serverError
        ? i18n.str`Server had an error`
        : i18n.str`Server didn't accept the request`,
      description: payload.error.description,
    });
  } else if (error instanceof Error) {
    saveError({
      title: i18n.str`Could not update account`,
      description: error.message,
    });
  } else {
    saveError({
      title: i18n.str`Error, please report`,
      debug: JSON.stringify(error),
    });
  }
}
