/*
 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,
  CoinDumpJson,
  CoinStatus,
  ExchangeListItem,
  ExchangeTosStatus,
  LogLevel,
  NotificationType,
  ScopeType,
  parseWithdrawUri,
  stringifyWithdrawExchange,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { 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 { Checkbox } from "../components/Checkbox.js";
import { SelectList } from "../components/SelectList.js";
import { Time } from "../components/Time.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";
import { Pages } from "../NavigationBar.js";
import { CoinInfo } from "@gnu-taler/taler-wallet-core/dbless";
import { ActiveTasksTable } from "../components/WalletActivity.js";

type CoinsInfo = CoinDumpJson["coins"];
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[];
};

export interface Props {
  // FIXME: Pending operations don't exist anymore.
}

function hashObjectId(o: any): string {
  return JSON.stringify(o);
}

export function DeveloperPage({ }: Props): VNode {
  const { i18n } = useTranslationContext();
  const [downloadedDatabase, setDownloadedDatabase] = useState<
    { time: Date; content: string } | undefined
  >(undefined);
  async function onExportDatabase(): Promise<void> {
    const db = await api.wallet.call(WalletApiOperation.ExportDb, {});
    const content = JSON.stringify(db);
    setDownloadedDatabase({
      time: new Date(),
      content,
    });
  }
  const api = useBackendContext();

  const fileRef = useRef<HTMLInputElement>(null);
  async function onImportDatabase(str: string): Promise<void> {
    await api.wallet.call(WalletApiOperation.ImportDb, {
      dump: JSON.parse(str),
    });
  }
  const [settings, updateSettings] = useSettings();
  const { safely } = useAlertContext();

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

  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 : [];

  useEffect(() => {
    return api.listener.onUpdateNotification(listenAllEvents, (ev) => {
      console.log("event", ev)
      return hook?.retry()
    });
  });

  const currencies: { [ex: string]: string } = {};
  const money_by_exchange = coins.reduce(
    (prev, cur) => {
      const denom = Amounts.parseOrThrow(cur.denom_value);
      if (!prev[cur.exchange_base_url]) {
        prev[cur.exchange_base_url] = [];
        currencies[cur.exchange_base_url] = denom.currency;
      }
      prev[cur.exchange_base_url].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.coin_status,
        from_refresh: cur.refresh_parent_coin_pub !== undefined,
        id: cur.coin_pub,
      });
      return prev;
    },
    {} as {
      [exchange_name: string]: CalculatedCoinfInfo[];
    },
  );

  const [tagName, setTagName] = useState("");
  const [logLevel, setLogLevel] = useState("info");
  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>
      {!exchangeList || !exchangeList.length ? (
        <div>
          <i18n.Translate>No exchange yet</i18n.Translate>
        </div>
      ) : (
        <Fragment>
          <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,
                  exchangePub: e.masterPub,
                });
                return (
                  <tr key={idx}>
                    <td>
                      <a href={!uri ? undefined : Pages.defaultCta({ 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">{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={() => {
                          api.wallet.call(
                            WalletApiOperation.UpdateExchangeEntry,
                            {
                              exchangeBaseUrl: e.exchangeBaseUrl,
                              force: true,
                            },
                          );
                        }}
                      >
                        Reload
                      </button>
                      <button
                        onClick={() => {
                          api.wallet.call(
                            WalletApiOperation.DeleteExchange,
                            {
                              exchangeBaseUrl: e.exchangeBaseUrl,
                            },
                          );
                        }}
                      >
                        Delete
                      </button>
                      <button
                        onClick={() => {
                          api.wallet.call(
                            WalletApiOperation.DeleteExchange,
                            {
                              exchangeBaseUrl: e.exchangeBaseUrl,
                              purge: true,
                            },
                          );
                        }}
                      >
                        Purge
                      </button>
                      {e.scopeInfo && e.masterPub && e.currency ?
                        (e.scopeInfo.type === ScopeType.Global ?
                          <button
                            onClick={() => {
                              api.wallet.call(
                                WalletApiOperation.RemoveGlobalCurrencyExchange,
                                {
                                  exchangeBaseUrl: e.exchangeBaseUrl,
                                  currency: e.currency!,
                                  exchangeMasterPub: e.masterPub!,
                                },
                              );
                            }}
                          >

                            Make regional
                          </button>
                          : e.scopeInfo.type === ScopeType.Auditor ?
                            undefined

                            : e.scopeInfo.type === ScopeType.Exchange ?
                              <button
                                onClick={() => {
                                  api.wallet.call(
                                    WalletApiOperation.AddGlobalCurrencyExchange,
                                    {
                                      exchangeBaseUrl: e.exchangeBaseUrl,
                                      currency: e.currency!,
                                      exchangeMasterPub: e.masterPub!,
                                    },
                                  );
                                }}
                              >

                                Make global
                              </button>
                              : undefined) : undefined
                      }
                      <button
                        onClick={() => {
                          api.wallet.call(
                            WalletApiOperation.SetExchangeTosForgotten,
                            {
                              exchangeBaseUrl: e.exchangeBaseUrl,
                            },
                          );
                        }}
                      >
                        Forget ToS
                      </button>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </Fragment>
      )}
      <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>

      <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>
  );
}

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();
  }
}
