/*
 This file is part of GNU Taler
 (C) 2022-2025 Taler Systems S.A.

 GNU Taler is free software; you can redistribute it and/or modify it under the
 terms of the GNU General Public License as published by the Free Software
 Foundation; either version 3, or (at your option) any later version.

 GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 A PARTICULAR PURPOSE.  See the GNU General Public License for more details.

 You should have received a copy of the GNU General Public License along with
 GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 */
import {
  assertUnreachable,
  KycCheckInformation,
  MeasureInformation,
  TalerError,
} from "@gnu-taler/taler-util";
import {
  FormDesign,
  FormUI,
  InternationalizationAPI,
  onComponentUnload,
  useForm,
  useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { CurrentMeasureTable } from "../../components/MeasuresTable.js";
import { MeasureDefinition, NewMeasure } from "../../components/NewMeasure.js";
import { useCurrentDecisionRequest } from "../../hooks/decision-request.js";
import { useServerMeasures } from "../../hooks/server-info.js";
import { computeMeasureInformation } from "../../utils/computeAvailableMesaures.js";

/**
 * Ask for more information, define new paths to proceed
 * @param param0
 * @returns
 */
export function Measures(): VNode {
  const [request, updateRequest] = useCurrentDecisionRequest();
  const [addMeasure, setAddMeasure] = useState<{
    isNew: boolean;
    template: Partial<MeasureDefinition>;
  }>();

  const measures = useServerMeasures();

  const measureBody =
    !measures || measures instanceof TalerError || measures.type === "fail"
      ? undefined
      : measures.body;

  const measureList = (
    !measureBody ? [] : Object.entries(measureBody.roots)
  ).map(convertToMeasureType);

  const requestCustomMeasures = request?.custom_measures ?? {};
  const customMeasures = Object.entries(requestCustomMeasures).map(
    convertToMeasureType,
  );

  const checkList = !measureBody ? [] : Object.entries(measureBody.checks);
  const simpleChecks = checkList
    .map(convertCheckToMeasureType)
    .filter((d): d is MeasureType => d !== undefined);

  const allMeasures: MeasureType[] = [
    ...measureList,
    ...customMeasures,
    ...simpleChecks,
  ];

  if (addMeasure) {
    return (
      <NewMeasure
        onCancel={() => {
          setAddMeasure(undefined);
        }}
        onChanged={() => {
          setAddMeasure(undefined);
        }}
        initial={addMeasure.template}
        isNew={addMeasure.isNew}
        onAdded={(m) => {
          const nm = (request.new_measures ?? []).filter((v) => v !== m);
          nm.push(m);
          updateRequest("added active measure", {
            new_measures: nm,
          });
          setAddMeasure(undefined);
        }}
        onRemoved={(m) => {
          const nm = (request.new_measures ?? []).filter((v) => v !== m);
          updateRequest("delete active measure", {
            new_measures: nm,
          });
          setAddMeasure(undefined);
        }}
      />
    );
  }

  return (
    <Fragment>
      {!allMeasures.length ? undefined : (
        <ActiveMeasureForm
          editMeasure={(template) => {
            template.name!;
            setAddMeasure({
              isNew: true,
              template,
            });
          }}
          measures={allMeasures}
          newMeasures={!request.new_measures ? [] : request.new_measures}
        />
      )}
      <ShowAllMeasures
        addNewMeasure={(template) => {
          setAddMeasure({
            isNew: true,
            template,
          });
        }}
        editMeasure={(template) => {
          setAddMeasure({
            isNew: false,
            template,
          });
        }}
      />
    </Fragment>
  );
}

function convertCheckToMeasureType([checkName, check]: [
  string,
  KycCheckInformation,
]): MeasureType | undefined {
  if (check.outputs.length === 0 && check.requires.length === 0) {
    return {
      type: "simple-check-form",
      name: `check-${checkName}`,
      checkName,
    };
  }
  return undefined;
}

const validChallengeType = ["email", "phone", "postal"];
function convertToMeasureType([name, measure]: [
  string,
  MeasureInformation,
]): MeasureType {
  if (measure.context && typeof measure.context === "object") {
    // @ts-expect-error measure is an object, should have a string key
    const challengeType = measure.context["challenge-type"] as string;
    if (validChallengeType.indexOf(challengeType) !== -1) {
      return {
        type: "verify-template",
        name,
        measure,
        // @ts-expect-error challenger type is validated
        challengerType: challengeType,
      };
    }
  }
  return {
    type: "normal",
    name,
  };
}

function ActiveMeasureForm({
  measures,
  editMeasure,
  newMeasures,
}: {
  measures: MeasureType[];
  newMeasures: string[];
  editMeasure: (m: Partial<MeasureDefinition>) => void;
}): VNode {
  const { i18n } = useTranslationContext();
  const [request, updateRequest] = useCurrentDecisionRequest();

  const design = formDesign(i18n, measures);

  const form = useForm<FormType>(design, { measures: newMeasures });
  const requestCustomMeasures = request?.custom_measures ?? {};

  onComponentUnload(() => {
    const newMeasures: string[] = [];
    const formMeasures = form.status.result.measures ?? [];
    for (const name of formMeasures) {
      newMeasures.push(name);
      const m = measures.find((d) => d.name === name)!;
      switch (m.type) {
        case "simple-check-form": {
          requestCustomMeasures[m.name] = {
            check_name: m.checkName,
          };
          break;
        }
        case "normal":
        case "verify-template": {
          break;
        }
        default: {
          assertUnreachable(m);
        }
      }
    }
    updateRequest("unload active measure", {
      new_measures: newMeasures,
      custom_measures: requestCustomMeasures,
    });
  });

  const selected = form.status.result.measures ?? [];

  const selectedVerifyMeasure = selected
    .map((s) => measures.find((d) => d.name === s))
    .filter((d) => d !== undefined && d.type === "verify-template")
    .filter((c) => requestCustomMeasures[c.name] === undefined);

  return (
    <Fragment>
      <FormUI design={design} model={form.model} />

      <div>
        {selectedVerifyMeasure.map((ver, ky) => {
          return (
            <button
              key={ky}
              onClick={() => {
                editMeasure({
                  check: ver.measure.check_name,
                  context: !ver.measure.context
                    ? []
                    : Object.entries(ver.measure.context).map(
                        ([key, value]) => ({
                          key,
                          type: "json",
                          value: JSON.stringify(value),
                        }),
                      ),
                  name: ver.name,
                  program: ver.measure.prog_name,
                });
              }}
              class="m-4  rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700 disabled:bg-gray-600"
            >
              <i18n.Translate>
                Configure verification measure: "{ver.name}"{" "}
              </i18n.Translate>
            </button>
          );
        })}
      </div>
    </Fragment>
  );
}

function ShowAllMeasures({
  addNewMeasure,
  editMeasure,
}: {
  addNewMeasure: (m: Partial<MeasureDefinition>) => void;
  editMeasure: (m: Partial<MeasureDefinition>) => void;
}): VNode {
  const { i18n } = useTranslationContext();
  const measures = useServerMeasures();

  const measureBody =
    !measures || measures instanceof TalerError || measures.type === "fail"
      ? undefined
      : measures.body;

  const [request] = useCurrentDecisionRequest();
  const haveCustomMeasures =
    Object.keys(request?.custom_measures ?? {}).length > 0;

  return (
    <div>
      <button
        onClick={() => {
          addNewMeasure({});
        }}
        class="m-4  rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700 disabled:bg-gray-600"
      >
        <i18n.Translate>Add custom measure</i18n.Translate>
      </button>
      {!haveCustomMeasures ? undefined : (
        <div class="divide-y divide-gray-200 overflow-x-scroll rounded-lg bg-white shadow-sm">
          <div class="p-2">
            <h1>
              <i18n.Translate>Custom measures</i18n.Translate>
            </h1>
          </div>
          <div class="p-2">
            <CurrentMeasureTable
              measures={computeMeasureInformation(measureBody, {
                measureMap: request.custom_measures,
              })}
              onSelect={(m) => {
                editMeasure({
                  check: m.type === "form" ? m.checkName : undefined,
                  context: !m.context
                    ? []
                    : Object.entries(m.context).map(([key, value]) => ({
                        key,
                        type: "json",
                        value: JSON.stringify(value),
                      })),
                  name: m.name,
                  program: m.type !== "info" ? m.programName : undefined,
                });
              }}
            />
          </div>
        </div>
      )}
      <div class="divide-y divide-gray-200 overflow-x-scroll rounded-lg bg-white shadow-sm">
        <div class="p-2">
          <h1>
            <i18n.Translate>Server measures</i18n.Translate>
          </h1>
        </div>
        <div class="p-2">
          <CurrentMeasureTable
            measures={computeMeasureInformation(measureBody)}
            onSelect={(m) => {
              addNewMeasure({
                check: m.type === "form" ? m.checkName : undefined,
                context: !m.context
                  ? []
                  : Object.entries(m.context).map(([key, value]) => ({
                      key,
                      type: "json",
                      value: JSON.stringify(value),
                    })),
                name: m.name,
                program: m.type !== "info" ? m.programName : undefined,
              });
            }}
          />
        </div>
      </div>
    </div>
  );
}

type MeasureType = NormalMeasure | SimpleCheckMeasure | VerifyMeasure;

/**
 * Normal measures are custom measures or server defined measure.
 * The name reference some measure already defined
 */
type NormalMeasure = {
  type: "normal";
  name: string;
};

/**
 * Simple check form measure are not yet defined but it should be automatically
 * added on submission.
 * This checks requires no context and output no information so it doesn't need
 * any program.
 */
type SimpleCheckMeasure = {
  type: "simple-check-form";
  name: string;
  checkName: string;
};

type VerifyMeasure = {
  type: "verify-template";
  name: string;
  measure: MeasureInformation;
  challengeType: "email" | "phone" | "postal";
};

type FormType = {
  measures: string[];
};

function formDesign(
  i18n: InternationalizationAPI,
  measureNames: MeasureType[],
): FormDesign {
  return {
    type: "single-column",
    fields: [
      {
        type: "selectMultiple",
        unique: true,
        choices: measureNames.map((me) => {
          switch (me.type) {
            case "normal": {
              return {
                label: me.name,
                value: me.name,
              };
            }
            case "simple-check-form": {
              return {
                label: `CHECK: ${me.checkName}`,
                value: `check-${me.checkName}`,
              };
            }
            case "verify-template": {
              return {
                label: me.name,
                value: me.name,
              };
            }
            default: {
              assertUnreachable(me);
            }
          }
        }),
        id: "measures",
        label: i18n.str`New measures`,
        help: i18n.str`Measures that are available/executed directly after the decision is confirmed.`,
      },
    ],
  };
}
