/*
 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 {
  AbsoluteTime,
  Codec,
  Duration,
  EddsaPrivP,
  HttpStatusCode,
  LockedAccount,
  OfficerAccount,
  OfficerId,
  OperationFail,
  OperationOk,
  buildCodecForObject,
  codecForAbsoluteTime,
  codecForString,
  createNewOfficerAccount,
  decodeCrock,
  encodeCrock,
  opFixedSuccess,
  opKnownFailure,
  unlockOfficerAccount
} from "@gnu-taler/taler-util";
import {
  buildStorageKey,
  useExchangeApiContext,
  useLocalStorage,
} from "@gnu-taler/web-util/browser";
import { useEffect, useMemo } from "preact/hooks";
import { usePreferences } from "./preferences.js";

const DEFAULT_SESSION_DURATION = Duration.fromSpec({
  // seconds: 10,
  hours: 1,
});

export interface Officer {
  account: LockedAccount;
  when: AbsoluteTime;
}

const codecForLockedAccount = codecForString() as Codec<LockedAccount>;

type OfficerAccountString = {
  id: string;
  strKey: string;
  unlocked: AbsoluteTime;
};

export const codecForOfficerAccount = (): Codec<OfficerAccountString> =>
  buildCodecForObject<OfficerAccountString>()
    .property("id", codecForString())
    .property("strKey", codecForString())
    .property("unlocked", codecForAbsoluteTime)
    .build("OfficerAccount");

export const codecForOfficer = (): Codec<Officer> =>
  buildCodecForObject<Officer>()
    .property("account", codecForLockedAccount)
    .property("when", codecForAbsoluteTime)
    .build("Officer");

export type OfficerState = OfficerNotReady | OfficerReady;
export type OfficerNotReady = OfficerNotFound | OfficerLocked;
export interface OfficerNotFound {
  state: "not-found";
  create: (password: string) => Promise<OperationOk<OfficerId>>;
}
export interface OfficerLocked {
  state: "locked";
  forget: () => OperationOk<void>;
  tryUnlock: (password: string) => Promise<OperationOk<void> | OperationFail<HttpStatusCode.Forbidden>>;
}
export interface OfficerReady {
  state: "ready";
  account: OfficerAccount;
  forget: () => OperationOk<void>;
  lock: () => OperationOk<void>;
  expiration: AbsoluteTime;
}

const OFFICER_KEY = buildStorageKey("officer", codecForOfficer());
const DEV_ACCOUNT_KEY = buildStorageKey(
  "account-dev",
  codecForOfficerAccount(),
);

export function useOfficer(): OfficerState {
  const {
    lib: { exchange: api },
  } = useExchangeApiContext();
  const [pref] = usePreferences();
  pref.keepSessionAfterReload;
  // dev account, is kept on reloaded.
  const accountStorage = useLocalStorage(DEV_ACCOUNT_KEY);
  const account = useMemo(() => {
    if (!accountStorage.value) return undefined;

    return {
      id: accountStorage.value.id as OfficerId,
      signingKey: decodeCrock(accountStorage.value.strKey) as EddsaPrivP,
      unlocked: accountStorage.value.unlocked,
    };
  }, [accountStorage.value?.id, accountStorage.value?.strKey]);

  const officerStorage = useLocalStorage(OFFICER_KEY);
  const officer = useMemo(() => {
    if (!officerStorage.value) return undefined;
    return officerStorage.value;
  }, [officerStorage.value?.account, officerStorage.value?.when.t_ms]);

  if (officer === undefined) {
    return {
      state: "not-found",
      create: async (pwd: string) => {
        const resp = await api.getSeed();
        const extraEntropy = resp.type === "ok" ? resp.body : new Uint8Array();

        const { id, safe, signingKey } = await createNewOfficerAccount(
          pwd,
          extraEntropy,
        );
        officerStorage.update({
          account: safe,
          when: AbsoluteTime.now(),
        });

        // accountStorage.update({ id, signingKey });
        const strKey = encodeCrock(signingKey);
        accountStorage.update({ id, strKey, unlocked: AbsoluteTime.now() });

        return opFixedSuccess(id);
      },
    };
  }

  if (account === undefined) {
    return {
      state: "locked",
      forget: () => {
        officerStorage.reset();
        return opFixedSuccess(undefined);
      },
      tryUnlock: async (pwd: string) => {
        try {
          const ac = await unlockOfficerAccount(officer.account, pwd);
          // accountStorage.update(ac);
          accountStorage.update({
            id: ac.id,
            strKey: encodeCrock(ac.signingKey),
            unlocked: AbsoluteTime.now(),
          });
          return opFixedSuccess(undefined);
        } catch (e) {
          const d = opKnownFailure(HttpStatusCode.Forbidden);
          return d;
        }
      },
    };
  }

  const expiration = AbsoluteTime.addDuration(
    account.unlocked,
    DEFAULT_SESSION_DURATION,
  );

  return {
    state: "ready",
    account,
    expiration,
    lock: () => {
      accountStorage.reset();
      return opFixedSuccess(undefined);
    },
    forget: () => {
      officerStorage.reset();
      accountStorage.reset();
      return opFixedSuccess(undefined);
    },
  };
}

export function useExpireSessionAfter1hr() {
  const officer = useOfficer();

  const session = officer.state !== "ready" ? undefined : officer;

  useEffect(() => {
    if (!session) return;
    const timeLeftBeforeExpiration = Duration.getRemaining(session.expiration);

    if (timeLeftBeforeExpiration.d_ms === "forever") return;

    const remain = timeLeftBeforeExpiration.d_ms;
    const timeoutId = setTimeout(async () => {
      session.lock();
    }, remain);
    return () => {
      clearTimeout(timeoutId);
    };
  }, [session]);
}
