/*
 This file is part of GNU Taler
 (C) 2022-2024 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 {
  AccessToken,
  AmountString,
  OperationOk,
  TalerCoreBankHttpClient,
  TalerCorebankApi,
  opFixedSuccess
} from "@gnu-taler/taler-util";
import {
  Attention,
  ButtonBetter,
  LocalNotificationBanner,
  RouteDefinition,
  useBankCoreApiContext,
  useLocalNotificationBetter,
  useTranslationContext
} from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
import { useState } from "preact/hooks";
import { useSessionState } from "../../hooks/session.js";
import { getTimeframesForDate } from "./AdminHome.js";

const TALER_SCREEN_ID = 124;

interface Props {
  routeCancel: RouteDefinition;
}

type Options = {
  dayMetric: boolean;
  hourMetric: boolean;
  monthMetric: boolean;
  yearMetric: boolean;
  compareWithPrevious: boolean;
  endOnFirstFail: boolean;
  includeHeader: boolean;
};

/**
 * Show histories of public accounts.
 */
export function DownloadStats({ routeCancel }: Props): VNode {
  const { i18n } = useTranslationContext();

  const { state: credentials } = useSessionState();
  const creds =
    credentials.status !== "loggedIn" || !credentials.isUserAdministrator
      ? undefined
      : credentials;
  const {
    lib: { bank: api },
  } = useBankCoreApiContext();

  const [options, setOptions] = useState<Options>({
    compareWithPrevious: true,
    dayMetric: true,
    endOnFirstFail: false,
    hourMetric: true,
    includeHeader: true,
    monthMetric: true,
    yearMetric: true,
  });
  const [lastStep, setLastStep] = useState<{ step: number; total: number }>();
  const [downloaded, setDownloaded] = useState<string>();
  const referenceDates = [new Date()];
  const [notification, safeFunctionHandler] = useLocalNotificationBetter();

  const download = safeFunctionHandler(
    async (token) => {
      setDownloaded(undefined);
      return fetchAllStatus(
        api,
        token,
        options,
        referenceDates,
        (step, total) => {
          setLastStep({ step, total });
        },
      );
    },
    lastStep !== undefined || !creds ? undefined : [creds.token],
  );
  download.onSuccess = (success) => {
    setDownloaded(success);
    setLastStep(undefined);
  };

  if (!creds) {
    return <i18n.Translate>only admin can download stats</i18n.Translate>;
  }

  return (
    <div>
      <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-6 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
        <LocalNotificationBanner notification={notification} />

        <div class="px-4 sm:px-0">
          <h2 class="text-base font-semibold leading-7 text-gray-900">
            <i18n.Translate>Download bank stats</i18n.Translate>
          </h2>
        </div>

        <form
          class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"
          autoCapitalize="none"
          autoCorrect="off"
          onSubmit={(e) => {
            e.preventDefault();
          }}
        >
          <div class="px-4 py-6 sm:p-8">
            <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
              <div class="sm:col-span-5">
                <div class="flex items-center justify-between">
                  <span class="flex flex-grow flex-col">
                    <span
                      class="text-sm text-black font-medium leading-6 "
                      id="availability-label"
                    >
                      <i18n.Translate>Include hour metric</i18n.Translate>
                    </span>
                  </span>
                  <button
                    type="button"
                    name={`hour switch`}
                    data-enabled={options.hourMetric}
                    class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
                    role="switch"
                    aria-checked="false"
                    aria-labelledby="availability-label"
                    aria-describedby="availability-description"
                    onClick={() => {
                      setOptions({
                        ...options,
                        hourMetric: !options.hourMetric,
                      });
                    }}
                  >
                    <span
                      aria-hidden="true"
                      data-enabled={options.hourMetric}
                      class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
                    ></span>
                  </button>
                </div>
              </div>
              <div class="sm:col-span-5">
                <div class="flex items-center justify-between">
                  <span class="flex flex-grow flex-col">
                    <span
                      class="text-sm text-black font-medium leading-6 "
                      id="availability-label"
                    >
                      <i18n.Translate>Include day metric</i18n.Translate>
                    </span>
                  </span>
                  <button
                    type="button"
                    name={`day switch`}
                    data-enabled={!!options.dayMetric}
                    class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
                    role="switch"
                    aria-checked="false"
                    aria-labelledby="availability-label"
                    aria-describedby="availability-description"
                    onClick={() => {
                      setOptions({ ...options, dayMetric: !options.dayMetric });
                    }}
                  >
                    <span
                      aria-hidden="true"
                      data-enabled={options.dayMetric}
                      class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
                    ></span>
                  </button>
                </div>
              </div>
              <div class="sm:col-span-5">
                <div class="flex items-center justify-between">
                  <span class="flex flex-grow flex-col">
                    <span
                      class="text-sm text-black font-medium leading-6 "
                      id="availability-label"
                    >
                      <i18n.Translate>Include month metric</i18n.Translate>
                    </span>
                  </span>
                  <button
                    type="button"
                    name={`month switch`}
                    data-enabled={!!options.monthMetric}
                    class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
                    role="switch"
                    aria-checked="false"
                    aria-labelledby="availability-label"
                    aria-describedby="availability-description"
                    onClick={() => {
                      setOptions({
                        ...options,
                        monthMetric: !options.monthMetric,
                      });
                    }}
                  >
                    <span
                      aria-hidden="true"
                      data-enabled={options.monthMetric}
                      class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
                    ></span>
                  </button>
                </div>
              </div>
              <div class="sm:col-span-5">
                <div class="flex items-center justify-between">
                  <span class="flex flex-grow flex-col">
                    <span
                      class="text-sm text-black font-medium leading-6 "
                      id="availability-label"
                    >
                      <i18n.Translate>Include year metric</i18n.Translate>
                    </span>
                  </span>
                  <button
                    type="button"
                    name={`year switch`}
                    data-enabled={!!options.yearMetric}
                    class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
                    role="switch"
                    aria-checked="false"
                    aria-labelledby="availability-label"
                    aria-describedby="availability-description"
                    onClick={() => {
                      setOptions({
                        ...options,
                        yearMetric: !options.yearMetric,
                      });
                    }}
                  >
                    <span
                      aria-hidden="true"
                      data-enabled={options.yearMetric}
                      class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
                    ></span>
                  </button>
                </div>
              </div>
              <div class="sm:col-span-5">
                <div class="flex items-center justify-between">
                  <span class="flex flex-grow flex-col">
                    <span
                      class="text-sm text-black font-medium leading-6 "
                      id="availability-label"
                    >
                      <i18n.Translate>Include table header</i18n.Translate>
                    </span>
                  </span>
                  <button
                    type="button"
                    name={`header switch`}
                    data-enabled={!!options.includeHeader}
                    class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
                    role="switch"
                    aria-checked="false"
                    aria-labelledby="availability-label"
                    aria-describedby="availability-description"
                    onClick={() => {
                      setOptions({
                        ...options,
                        includeHeader: !options.includeHeader,
                      });
                    }}
                  >
                    <span
                      aria-hidden="true"
                      data-enabled={options.includeHeader}
                      class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
                    ></span>
                  </button>
                </div>
              </div>
              <div class="sm:col-span-5">
                <div class="flex items-center justify-between">
                  <span class="flex flex-grow flex-col">
                    <span
                      class="text-sm text-black font-medium leading-6 "
                      id="availability-label"
                    >
                      <i18n.Translate>
                        Add previous metric for compare
                      </i18n.Translate>
                    </span>
                  </span>
                  <button
                    type="button"
                    name={`compare switch`}
                    data-enabled={!!options.compareWithPrevious}
                    class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
                    role="switch"
                    aria-checked="false"
                    aria-labelledby="availability-label"
                    aria-describedby="availability-description"
                    onClick={() => {
                      setOptions({
                        ...options,
                        compareWithPrevious: !options.compareWithPrevious,
                      });
                    }}
                  >
                    <span
                      aria-hidden="true"
                      data-enabled={options.compareWithPrevious}
                      class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
                    ></span>
                  </button>
                </div>
              </div>
              <div class="sm:col-span-5">
                <div class="flex items-center justify-between">
                  <span class="flex flex-grow flex-col">
                    <span
                      class="text-sm text-black font-medium leading-6 "
                      id="availability-label"
                    >
                      <i18n.Translate>Fail on first error</i18n.Translate>
                    </span>
                  </span>
                  <button
                    type="button"
                    name={`fail switch`}
                    data-enabled={!!options.endOnFirstFail}
                    class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
                    role="switch"
                    aria-checked="false"
                    aria-labelledby="availability-label"
                    aria-describedby="availability-description"
                    onClick={() => {
                      setOptions({
                        ...options,
                        endOnFirstFail: !options.endOnFirstFail,
                      });
                    }}
                  >
                    <span
                      aria-hidden="true"
                      data-enabled={options.endOnFirstFail}
                      class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
                    ></span>
                  </button>
                </div>
              </div>
            </div>
          </div>

          <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
            <a
              name="cancel"
              href={routeCancel.url({})}
              class="text-sm font-semibold leading-6 text-gray-900"
            >
              <i18n.Translate>Cancel</i18n.Translate>
            </a>
            <ButtonBetter
              type="submit"
              name="download"
              class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
              onClick={download}
            >
              <i18n.Translate>Download</i18n.Translate>
            </ButtonBetter>
          </div>
        </form>
      </div>
      {!lastStep || lastStep.step === lastStep.total ? (
        <div class="h-5 mb-5" />
      ) : (
        <div>
          <div class="relative mb-5 h-5 rounded-full bg-gray-200">
            <div
              class={`h-full animate-pulse rounded-full bg-blue-500 w-[${Math.round(
                (lastStep.step / lastStep.total) * 100,
              )}%]`}
            >
              <span class="absolute inset-0 flex items-center justify-center text-xs font-semibold text-white">
                <i18n.Translate>
                  downloading...{" "}
                  {Math.round((lastStep.step / lastStep.total) * 100)}
                </i18n.Translate>
              </span>
            </div>
          </div>
        </div>
      )}
      {!downloaded ? (
        <div class="h-5 mb-5" />
      ) : (
        <a
          href={
            "data:text/plain;charset=utf-8," + encodeURIComponent(downloaded)
          }
          name="save file"
          download={"bank-stats.csv"}
        >
          <Attention title={i18n.str`Download completed`}>
            <i18n.Translate>
              Click here to save the file in your computer.
            </i18n.Translate>
          </Attention>
        </a>
      )}
    </div>
  );
}

async function fetchAllStatus(
  api: TalerCoreBankHttpClient,
  token: AccessToken,
  options: Options,
  references: Date[],
  progress: (current: number, total: number) => void,
): Promise<OperationOk<string>> {
  const allMetrics: TalerCorebankApi.MonitorTimeframeParam[] = [];
  if (options.hourMetric) {
    allMetrics.push(TalerCorebankApi.MonitorTimeframeParam.hour);
  }
  if (options.dayMetric) {
    allMetrics.push(TalerCorebankApi.MonitorTimeframeParam.day);
  }
  if (options.monthMetric) {
    allMetrics.push(TalerCorebankApi.MonitorTimeframeParam.month);
  }
  if (options.yearMetric) {
    allMetrics.push(TalerCorebankApi.MonitorTimeframeParam.year);
  }

  /**
   * convert request into frames
   */
  const allFrames = allMetrics.flatMap((timeframe) =>
    references.map((reference) => ({
      reference,
      timeframe,
      moment: getTimeframesForDate(reference, timeframe),
    })),
  );
  const total = allFrames.length;

  /**
   * call API for info
   */
  const allInfo = await allFrames.reduce(
    async (prev, frame, index) => {
      const accumulatedMap = await prev;
      progress(index, total);
      // await delay()
      const previous = options.compareWithPrevious
        ? await api.getMonitor(token, {
            timeframe: frame.timeframe,
            date: frame.moment.previous,
          })
        : undefined;

      if (previous && previous.type === "fail" && options.endOnFirstFail) {
        return accumulatedMap; //skip
      }

      const current = await api.getMonitor(token, {
        timeframe: frame.timeframe,
        date: frame.moment.current,
      });

      if (current.type === "fail" && options.endOnFirstFail) {
        return accumulatedMap; //skip
      }

      const metricName =
        TalerCorebankApi.MonitorTimeframeParam[allMetrics[index]];
      accumulatedMap[metricName] = {
        reference: frame.reference,
        current: current.type !== "ok" ? undefined : current.body,
        previous:
          !previous || previous.type !== "ok" ? undefined : previous.body,
      };
      return accumulatedMap;
    },
    Promise.resolve({} as Record<string, Data>),
  );
  progress(total, total);

  /**
   * convert into table format
   *
   */
  const table: Array<string[]> = [];
  if (options.includeHeader) {
    table.push([
      "date",
      "metric",
      "reference",
      "talerInCount",
      "talerInVolume",
      "talerOutCount",
      "talerOutVolume",
      "cashinCount",
      "cashinFiatVolume",
      "cashinRegionalVolume",
      "cashoutCount",
      "cashoutFiatVolume",
      "cashoutRegionalVolume",
    ]);
  }
  Object.entries(allInfo).forEach(([name, data]) => {
    if (data.current) {
      const row: TableRow = {
        date: data.reference.getTime(),
        metric: name,
        reference: "current",
        ...dataToRow(data.current),
      };
      table.push(Object.values(row) as string[]);
    }

    if (data.previous) {
      const row: TableRow = {
        date: data.reference.getTime(),
        metric: name,
        reference: "previous",
        ...dataToRow(data.previous),
      };
      table.push(Object.values(row) as string[]);
    }
  });

  const csv = table.reduce((acc, row) => {
    return acc + row.join(",") + "\n";
  }, "");

  return opFixedSuccess(csv);
}

type JustData = Omit<Omit<Omit<TableRow, "metric">, "date">, "reference">;
function dataToRow(info: TalerCorebankApi.MonitorResponse): JustData {
  return {
    talerInCount: info.talerInCount,
    talerInVolume: info.talerInVolume,
    talerOutCount: info.talerOutCount,
    talerOutVolume: info.talerOutVolume,
    cashinCount: info.type === "no-conversions" ? undefined : info.cashinCount,
    cashinFiatVolume:
      info.type === "no-conversions" ? undefined : info.cashinFiatVolume,
    cashinRegionalVolume:
      info.type === "no-conversions" ? undefined : info.cashinRegionalVolume,
    cashoutCount:
      info.type === "no-conversions" ? undefined : info.cashoutCount,
    cashoutFiatVolume:
      info.type === "no-conversions" ? undefined : info.cashoutFiatVolume,
    cashoutRegionalVolume:
      info.type === "no-conversions" ? undefined : info.cashoutRegionalVolume,
  };
}

type Data = {
  reference: Date;
  previous: TalerCorebankApi.MonitorResponse | undefined;
  current: TalerCorebankApi.MonitorResponse | undefined;
};
type TableRow = {
  date: number;
  metric: string;
  reference: "current" | "previous";
  cashinCount?: number;
  cashinRegionalVolume?: AmountString;
  cashinFiatVolume?: AmountString;
  cashoutCount?: number;
  cashoutRegionalVolume?: AmountString;
  cashoutFiatVolume?: AmountString;
  talerInCount: number;
  talerInVolume: AmountString;
  talerOutCount: number;
  talerOutVolume: AmountString;
};
