/*
 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 {
  AbsoluteTime,
  Amounts,
  CoinStatus,
  ExchangeListItem,
  ExchangeTosStatus,
  HostPortPath,
  LogLevel,
  NotificationType,
  ScopeType,
  stringifyWithdrawExchange,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import {
  encodeCrockForURI,
  useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
import { Fragment, VNode, h } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
import { Pages } from "../NavigationBar.js";
import { Checkbox } from "../components/Checkbox.js";
import { SelectList } from "../components/SelectList.js";
import { Time } from "../components/Time.js";
import { ActiveTasksTable } from "../components/WalletActivity.js";
import {
  DestructiveText,
  LinkPrimary,
  NotifyUpdateFadeOut,
  SubTitle,
  SuccessText,
  WarningText,
} from "../components/styled/index.js";
import { useAlertContext } from "../context/alert.js";
import { useBackendContext } from "../context/backend.js";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
import { useSettings } from "../hooks/useSettings.js";
import { Button } from "../mui/Button.js";
import { Grid } from "../mui/Grid.js";
import { Paper } from "../mui/Paper.js";
import { TextField } from "../mui/TextField.js";

type CalculatedCoinfInfo = {
  // ageKeysCount: number | undefined;
  denom_value: number;
  denom_fraction: number;
  //remain_value: number;
  status: string;
  from_refresh: boolean;
  id: string;
};

type SplitedCoinInfo = {
  spent: CalculatedCoinfInfo[];
  usable: CalculatedCoinfInfo[];
};

const listenAllEvents = Array.from<NotificationType>({ length: 1 });

function ExchangeActions({}: {}): VNode {
  const [time, reload] = useState<number>();
  const { i18n } = useTranslationContext();
  const api = useBackendContext();
  const hook = useAsyncAsHook(async () => {
    const list = await api.wallet.call(WalletApiOperation.ListExchanges, {});
    return { exchanges: list.exchanges };
  }, [time]);
  const exchangeList = hook && !hook.hasError ? hook.response.exchanges : [];

  if (!exchangeList || !exchangeList.length) {
    return (
      <div>
        <i18n.Translate>No exchange yet</i18n.Translate>
      </div>
    );
  }
  return (
    <table>
      <thead>
        <tr>
          <th>
            <i18n.Translate>Currency</i18n.Translate>
          </th>
          <th>
            <i18n.Translate>URL</i18n.Translate>
          </th>
          <th>
            <i18n.Translate>Status</i18n.Translate>
          </th>
          <th>
            <i18n.Translate>Terms of Service</i18n.Translate>
          </th>
          <th>
            <i18n.Translate>Last Update</i18n.Translate>
          </th>
          <th>
            <i18n.Translate>Actions</i18n.Translate>
          </th>
        </tr>
      </thead>
      <tbody>
        {exchangeList.map((e, idx) => {
          function TosStatus(): VNode {
            switch (e.tosStatus) {
              case ExchangeTosStatus.Accepted:
                return (
                  <SuccessText>
                    <i18n.Translate>ok</i18n.Translate>
                  </SuccessText>
                );
              case ExchangeTosStatus.Pending:
                return (
                  <WarningText>
                    <i18n.Translate>pending</i18n.Translate>
                  </WarningText>
                );
              case ExchangeTosStatus.Proposed:
                return <i18n.Translate>proposed</i18n.Translate>;
              default:
                return (
                  <DestructiveText>
                    <i18n.Translate>
                      unknown (exchange status should be updated)
                    </i18n.Translate>
                  </DestructiveText>
                );
            }
          }
          const uri = !e.masterPub
            ? undefined
            : stringifyWithdrawExchange({
                exchangeBaseUrl: e.exchangeBaseUrl as HostPortPath,
              });
          return (
            <tr key={idx}>
              <td>
                <a
                  href={
                    !uri
                      ? undefined
                      : `#${Pages.defaultCta({
                          uri: encodeCrockForURI(uri),
                        })}`
                  }
                >
                  {e.scopeInfo
                    ? `${e.scopeInfo.currency} (${
                        e.scopeInfo.type === ScopeType.Global
                          ? "global"
                          : "regional"
                      })`
                    : e.currency}
                </a>
              </td>
              <td>
                <a
                  href={new URL(`/keys`, e.exchangeBaseUrl).href}
                  target="_blank"
                  rel="noreferrer"
                >
                  {e.exchangeBaseUrl}
                </a>
              </td>
              <td>
                {e.exchangeEntryStatus} / {e.exchangeUpdateStatus}
              </td>
              <td>
                <TosStatus />
              </td>
              <td>
                {e.lastUpdateTimestamp
                  ? AbsoluteTime.toIsoString(
                      AbsoluteTime.fromPreciseTimestamp(e.lastUpdateTimestamp),
                    )
                  : "never"}
              </td>
              <td>
                <button
                  onClick={async () => {
                    await api.wallet.call(
                      WalletApiOperation.UpdateExchangeEntry,
                      {
                        exchangeBaseUrl: e.exchangeBaseUrl,
                        force: true,
                      },
                    );
                    reload(new Date().getTime());
                  }}
                >
                  <i18n.Translate>Reload</i18n.Translate>
                </button>
                <button
                  onClick={async () => {
                    await api.wallet.call(WalletApiOperation.DeleteExchange, {
                      exchangeBaseUrl: e.exchangeBaseUrl,
                    });
                    reload(new Date().getTime());
                  }}
                >
                  <i18n.Translate>Delete</i18n.Translate>
                </button>
                <button
                  onClick={async () => {
                    await api.wallet.call(WalletApiOperation.DeleteExchange, {
                      exchangeBaseUrl: e.exchangeBaseUrl,
                      purge: true,
                    });
                    reload(new Date().getTime());
                  }}
                >
                  <i18n.Translate>Purge</i18n.Translate>
                </button>
                {e.scopeInfo && e.masterPub && e.currency ? (
                  e.scopeInfo.type === ScopeType.Global ? (
                    <button
                      onClick={async () => {
                        await api.wallet.call(
                          WalletApiOperation.RemoveGlobalCurrencyExchange,
                          {
                            exchangeBaseUrl: e.exchangeBaseUrl,
                            currency: e.currency!,
                            exchangeMasterPub: e.masterPub!,
                          },
                        );
                        reload(new Date().getTime());
                      }}
                    >
                      <i18n.Translate>Make regional</i18n.Translate>
                    </button>
                  ) : e.scopeInfo.type === ScopeType.Auditor ? undefined : e
                      .scopeInfo.type === ScopeType.Exchange ? (
                    <button
                      onClick={async () => {
                        await api.wallet.call(
                          WalletApiOperation.AddGlobalCurrencyExchange,
                          {
                            exchangeBaseUrl: e.exchangeBaseUrl,
                            currency: e.currency!,
                            exchangeMasterPub: e.masterPub!,
                          },
                        );
                        reload(new Date().getTime());
                      }}
                    >
                      <i18n.Translate>Make global</i18n.Translate>
                    </button>
                  ) : undefined
                ) : undefined}
                <button
                  disabled={e.tosStatus !== ExchangeTosStatus.Accepted}
                  onClick={async () => {
                    await api.wallet.call(
                      WalletApiOperation.SetExchangeTosForgotten,
                      {
                        exchangeBaseUrl: e.exchangeBaseUrl,
                      },
                    );
                    reload(new Date().getTime());
                  }}
                >
                  <i18n.Translate>Forget ToS</i18n.Translate>
                </button>
              </td>
            </tr>
          );
        })}
      </tbody>
    </table>
  );
}

export function DeveloperPage(): VNode {
  const { i18n } = useTranslationContext();
  const [downloadedDatabase, setDownloadedDatabase] = useState<
    { time: Date; content: string } | undefined
  >(undefined);
  const [tagName, setTagName] = useState("");
  const [experiment, setExperiment] = useState("");
  const [logLevel, setLogLevel] = useState("info");
  const api = useBackendContext();
  const fileRef = useRef<HTMLInputElement>(null);
  const [settings, updateSettings] = useSettings();
  const { safely } = useAlertContext();
  useEffect(() => {
    return api.listener.onUpdateNotification(listenAllEvents, (ev) => {
      console.log("event", ev);
      return hook?.retry();
    });
  });

  async function onExportDatabase(): Promise<void> {
    const db = await api.wallet.call(WalletApiOperation.ExportDb, {});
    const content = JSON.stringify(db);
    setDownloadedDatabase({
      time: new Date(),
      content,
    });
  }

  async function onImportDatabase(str: string): Promise<void> {
    await api.wallet.call(WalletApiOperation.ImportDb, {
      dump: JSON.parse(str),
    });
  }

  const hook = useAsyncAsHook(async () => {
    const list = await api.wallet.call(WalletApiOperation.ListExchanges, {});
    const version = await api.wallet.call(WalletApiOperation.GetVersion, {});
    const coins = await api.wallet.call(WalletApiOperation.DumpCoins, {});
    return { exchanges: list.exchanges, version, coins };
  });
  const exchangeList = hook && !hook.hasError ? hook.response.exchanges : [];
  const coins = hook && !hook.hasError ? hook.response.coins.coins : [];

  const currencies: { [ex: string]: string } = {};
  const money_by_exchange = coins.reduce(
    (prev, cur) => {
      const denom = Amounts.parseOrThrow(cur.denomValue);
      if (!prev[cur.exchangeBaseUrl]) {
        prev[cur.exchangeBaseUrl] = [];
        currencies[cur.exchangeBaseUrl] = denom.currency;
      }
      prev[cur.exchangeBaseUrl].push({
        // ageKeysCount: cur.ageCommitmentProof?.proof.privateKeys.length,
        denom_value: denom.value,
        denom_fraction: denom.fraction,
        // remain_value: parseFloat(
        //   Amounts.stringifyValue(Amounts.parseOrThrow(cur.remaining_value)),
        // ),
        status: cur.coinStatus,
        from_refresh: cur.refreshParentCoinPub !== undefined,
        id: cur.coinPub,
      });
      return prev;
    },
    {} as {
      [exchange_name: string]: CalculatedCoinfInfo[];
    },
  );

  return (
    <div>
      <p>
        <i18n.Translate>Debug tools</i18n.Translate>:
      </p>
      <Grid container justifyContent="space-between" spacing={1} size={4}>
        <Grid item>
          <Button
            variant="contained"
            onClick={() =>
              confirmReset(
                i18n.str`Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE ALL YOUR COINS?`,
                () => api.background.call("resetDb", undefined),
              )
            }
          >
            <i18n.Translate>reset</i18n.Translate>
          </Button>
        </Grid>
        <Grid item>
          <Button
            variant="contained"
            onClick={() =>
              confirmReset(
                i18n.str`TESTING: This may delete all your coin, proceed with caution`,
                () => api.background.call("runGarbageCollector", undefined),
              )
            }
          >
            <i18n.Translate>run gc</i18n.Translate>
          </Button>
        </Grid>
        <Grid item>
          <Button
            variant="contained"
            onClick={async () => fileRef?.current?.click()}
          >
            <i18n.Translate>import database</i18n.Translate>
          </Button>
        </Grid>
        <Grid item>
          <input
            ref={fileRef}
            style={{ display: "none" }}
            type="file"
            onChange={async (e) => {
              const f: FileList | null = e.currentTarget.files;
              if (!f || f.length != 1) {
                return Promise.reject();
              }
              const buf = await f[0].arrayBuffer();
              const str = new Uint8Array(buf).reduce(
                (data, byte) => data + String.fromCharCode(byte),
                "",
              );
              return onImportDatabase(str);
            }}
          />
          <Button variant="contained" onClick={onExportDatabase}>
            <i18n.Translate>export database</i18n.Translate>
          </Button>
        </Grid>
        <Grid item>
          <Button
            variant="contained"
            onClick={async () => {
              const result = await Promise.all(
                exchangeList.map(async (exchange) => {
                  const url = exchange.exchangeBaseUrl;
                  const oldKeys = JSON.stringify(
                    await (await fetch(`${url}keys`)).json(),
                  );
                  const newKeys = JSON.stringify(
                    await (
                      await fetch(`${url}keys`, { cache: "no-cache" })
                    ).json(),
                  );
                  return oldKeys !== newKeys;
                }),
              );
              const ex = exchangeList.filter((e, i) => result[i]);
              if (!ex.length) {
                alert("no exchange was outdated");
              } else {
                alert(`found some exchange out of date: ${result.join(", ")}`);
              }
            }}
          >
            <i18n.Translate>Clear exchange key cache</i18n.Translate>
          </Button>
        </Grid>{" "}
      </Grid>
      {downloadedDatabase && (
        <div>
          <i18n.Translate>
            Database exported at{" "}
            <Time
              timestamp={AbsoluteTime.fromMilliseconds(
                downloadedDatabase.time.getTime(),
              )}
              format="yyyy/MM/dd HH:mm:ss"
            />{" "}
            <a
              href={`data:text/plain;charset=utf-8;base64,${toBase64(
                downloadedDatabase.content,
              )}`}
              download={`taler-wallet-database-${format(
                downloadedDatabase.time,
                "yyyy/MM/dd_HH:mm",
              )}.json`}
            >
              <i18n.Translate>click here</i18n.Translate>
            </a>{" "}
            to download
          </i18n.Translate>
        </div>
      )}
      <Checkbox
        label={i18n.str`Inject Taler support in all pages`}
        name="inject"
        description={
          <i18n.Translate>
            Enabling this option will make `window.taler` be available in all
            sites
          </i18n.Translate>
        }
        enabled={settings.injectTalerSupport!}
        onToggle={safely("update support injection", async () => {
          updateSettings("injectTalerSupport", !settings.injectTalerSupport);
        })}
      />

      <SubTitle>
        <i18n.Translate>Exchange Entries</i18n.Translate>
      </SubTitle>
      <ExchangeActions />

      <div style={{ display: "flex", justifyContent: "space-between" }}>
        <div />
        <LinkPrimary href={`#${Pages.settingsExchangeAdd({})}`}>
          <i18n.Translate>Add an exchange</i18n.Translate>
        </LinkPrimary>
      </div>

      <Paper style={{ padding: 10, margin: 10 }}>
        <h3>Logging</h3>
        <div>
          <TextField
            label="Tag name"
            placeholder="wallet.ts"
            variant="filled"
            // error={subject.error}
            required
            value={tagName}
            onChange={setTagName}
          />
          <SelectList
            label={i18n.str`Log levels`}
            list={{
              trace: "TRACE",
              info: "INFO",
              error: "ERROR",
            }}
            name="logLevel"
            value={logLevel}
            onChange={(v) => setLogLevel(v)}
          />
        </div>
        <Button
          variant="contained"
          onClick={async () => {
            api.background.call("setLoggingLevel", {
              tag: tagName,
              level: logLevel as LogLevel,
            });
          }}
        >
          Set log level
        </Button>
      </Paper>

      <Paper style={{ padding: 10, margin: 10 }}>
        <h3>Dev Experiment</h3>
        <div>
          <TextField
            label="Experiment name"
            placeholder="taler://dev-experiment"
            variant="filled"
            // error={subject.error}
            required
            value={experiment}
            onChange={setExperiment}
          />
        </div>
        <Button
          variant="contained"
          onClick={async () => {
            await api.wallet.call(WalletApiOperation.ApplyDevExperiment, {
              devExperimentUri: `taler://dev-experiment/${experiment}`,
            });
          }}
        >
          Apply
        </Button>
      </Paper>

      <br />
      <p>
        <i18n.Translate>Coins</i18n.Translate>:
      </p>
      {Object.keys(money_by_exchange).map((ex, idx) => {
        const allcoins = money_by_exchange[ex];
        allcoins.sort((a, b) => {
          if (b.denom_value !== a.denom_value) {
            return b.denom_value - a.denom_value;
          }
          return b.denom_fraction - a.denom_fraction;
        });

        const coins = allcoins.reduce(
          (prev, cur) => {
            if (cur.status === CoinStatus.Fresh) prev.usable.push(cur);
            if (cur.status === CoinStatus.Dormant) prev.spent.push(cur);
            return prev;
          },
          {
            spent: [],
            usable: [],
          } as SplitedCoinInfo,
        );

        return (
          <ShowAllCoins
            key={idx}
            coins={coins}
            ex={ex}
            currencies={currencies}
          />
        );
      })}
      <br />
      <NotifyUpdateFadeOut>
        <ActiveTasksTable />
      </NotifyUpdateFadeOut>
    </div>
  );
}

function ShowAllCoins({
  ex,
  coins,
  currencies,
}: {
  ex: string;
  coins: SplitedCoinInfo;
  currencies: { [ex: string]: string };
}): VNode {
  const { i18n } = useTranslationContext();
  const [collapsedSpent, setCollapsedSpent] = useState(true);
  const [collapsedUnspent, setCollapsedUnspent] = useState(false);
  const totalUsable = coins.usable.reduce(
    (prev, cur) =>
      Amounts.add(prev, {
        currency: "NONE",
        fraction: cur.denom_fraction,
        value: cur.denom_value,
      }).amount,
    Amounts.zeroOfCurrency("NONE"),
  );
  const totalSpent = coins.spent.reduce(
    (prev, cur) =>
      Amounts.add(prev, {
        currency: "NONE",
        fraction: cur.denom_fraction,
        value: cur.denom_value,
      }).amount,
    Amounts.zeroOfCurrency("NONE"),
  );
  return (
    <Fragment>
      <p>
        <b>{ex}</b>: {Amounts.stringifyValue(totalUsable)} {currencies[ex]}
      </p>
      <p>
        spent: {Amounts.stringifyValue(totalSpent)} {currencies[ex]}
      </p>
      <p onClick={() => setCollapsedUnspent(true)}>
        <b>
          <i18n.Translate>usable coins</i18n.Translate>
        </b>
      </p>
      {collapsedUnspent ? (
        <div onClick={() => setCollapsedUnspent(false)}>click to show</div>
      ) : (
        <table>
          <tr>
            <td>
              <i18n.Translate>id</i18n.Translate>
            </td>
            <td>
              <i18n.Translate>denom</i18n.Translate>
            </td>
            <td>
              <i18n.Translate>status</i18n.Translate>
            </td>
            <td>
              <i18n.Translate>from refresh?</i18n.Translate>
            </td>
            <td>
              <i18n.Translate>age key count</i18n.Translate>
            </td>
          </tr>
          {coins.usable.map((c, idx) => {
            return (
              <tr key={idx}>
                <td>{c.id.substring(0, 5)}</td>
                <td>
                  {Amounts.stringifyValue({
                    value: c.denom_value,
                    fraction: c.denom_fraction,
                    currency: "ANY",
                  })}
                </td>
                <td>{c.status}</td>
                <td>{c.from_refresh ? "true" : "false"}</td>
                {/* <td>{String(c.ageKeysCount)}</td> */}
              </tr>
            );
          })}
        </table>
      )}
      <p onClick={() => setCollapsedSpent(true)}>
        <i18n.Translate>spent coins</i18n.Translate>
      </p>
      {collapsedSpent ? (
        <div onClick={() => setCollapsedSpent(false)}>
          <i18n.Translate>click to show</i18n.Translate>
        </div>
      ) : (
        <table>
          <tr>
            <td>
              <i18n.Translate>id</i18n.Translate>
            </td>
            <td>
              <i18n.Translate>denom</i18n.Translate>
            </td>
            <td>
              <i18n.Translate>status</i18n.Translate>
            </td>
            <td>
              <i18n.Translate>from refresh?</i18n.Translate>
            </td>
          </tr>
          {coins.spent.map((c, idx) => {
            return (
              <tr key={idx}>
                <td>{c.id.substring(0, 5)}</td>
                <td>{c.denom_value}</td>
                <td>{c.status}</td>
                <td>{c.from_refresh ? "true" : "false"}</td>
              </tr>
            );
          })}
        </table>
      )}
    </Fragment>
  );
}

/**
 *
 * @param str
 * @deprecated FIXME: use a better base64 function
 * @returns
 */
function toBase64(str: string): string {
  return btoa(
    encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) {
      return String.fromCharCode(parseInt(p1, 16));
    }),
  );
}

export async function confirmReset(
  confirmTheResetMessage: string,
  cb: () => Promise<void>,
): Promise<void> {
  if (confirm(confirmTheResetMessage)) {
    await cb();
    window.close();
  }
}
