/*
 This file is part of GNU Taler
 (C) 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, Duration, j2s, Logger } from "@gnu-taler/taler-util";
import * as http from "node:http";
import { inflateSync } from "node:zlib";
import {
  readBodyBytes,
  readBodyStr,
  respondJson,
  splitInTwoAt,
} from "./http-server.js";

const logger = new Logger("fake-challenger.ts");

export interface TestfakeChallengerService {
  stop: () => void;
  fakeVerification(nonce: string, attributes: Record<string, string>): void;
  getSetupRequest(nonce: string): any;
}

/**
 * Testfake for the kyc service that the exchange talks to.
 */
export async function startFakeChallenger(options: {
  port: number;
  addressType: string;
}): Promise<TestfakeChallengerService> {
  let nextNonceId = 1;

  const addressType = options.addressType;

  const infoForNonceId: Map<
    number,
    {
      // Faked "validated" address
      address?: any;
      // Setup message
      setup?: any;
    }
  > = new Map();

  const server = http.createServer(async (req, res) => {
    const requestUrl = req.url!;
    logger.info(`fake-challenger: got ${req.method} request, ${requestUrl}`);

    const [path, query] = splitInTwoAt(requestUrl, "?");

    const qp = new URLSearchParams(query);

    if (path.startsWith("/authorize/")) {
      const nonce = path.substring("/authorize/".length);
      logger.info(`got authorize request with noce ${nonce}`);
      // Usually this would render some HTML page for the user to log in,
      // but we return JSON here.
      const redirUriUnparsed = qp.get("redirect_uri");
      if (!redirUriUnparsed) {
        throw Error("missing redirect_url");
      }
      const state = qp.get("state");
      if (!state) {
        throw Error("missing state");
      }
      const redirUri = new URL(redirUriUnparsed);
      redirUri.searchParams.set("code", `code-${nonce}`);
      redirUri.searchParams.set("state", state);
      respondJson(res, 200, {
        // Return so that the nonce can be used to fake the address validation.
        nonce,
        redirect_url: redirUri.href,
      });
    } else if (path === "/token") {
      const reqBody = await readBodyStr(req);
      logger.info("login request body:", reqBody);
      const qs = new URLSearchParams(reqBody);
      const code = qs.get("code");
      if (typeof code !== "string") {
        throw Error("expected code");
      }
      respondJson(res, 200, {
        access_token: `tok-${code}`,
        token_type: "Bearer",
        expires_in: 60 * 60,
      });
    } else if (path === "/info") {
      const ath = req.headers.authorization;
      logger.info(`authorization header: ${ath}`);
      const tokPrefix = "Bearer tok-code-nonce-";
      if (!ath?.startsWith(tokPrefix)) {
        throw Error("invalid token");
      }
      const nonce = Number(ath.substring(tokPrefix.length));
      logger.info(`nonce for /info is ${nonce}`);
      const myInfo = infoForNonceId.get(nonce);
      if (!myInfo) {
        respondJson(res, 400, {
          error: "invalid_request",
        });
        return;
      }
      respondJson(res, 200, {
        id: nonce,
        address: myInfo.address,
        address_type: addressType,
        expires: AbsoluteTime.toProtocolTimestamp(
          AbsoluteTime.addDuration(
            AbsoluteTime.now(),
            Duration.fromSpec({ days: 1 }),
          ),
        ),
      });
    } else if (path.startsWith("/setup/")) {
      let reqBody: string;
      if (req.headers["content-encoding"] === "deflate") {
        const bodyEnc = await readBodyBytes(req);
        const td = new TextDecoder();
        reqBody = td.decode(inflateSync(bodyEnc));
      } else {
        reqBody = await readBodyStr(req);
      }
      let bodyJson: any;
      if (reqBody.length > 0) {
        bodyJson = JSON.parse(reqBody);
      }
      logger.info(`setup content-encoding: ${req.headers["content-encoding"]}`);
      logger.info(`setup request body: ${j2s(bodyJson)}`);

      const clientId = path.substring("/setup/".length);
      logger.info(`client ID: ${clientId}`);
      res.writeHead(200, { "Content-Type": "application/json" });
      let myNonce = nextNonceId++;
      infoForNonceId.set(myNonce, {
        setup: bodyJson,
      });
      res.end(
        JSON.stringify({
          nonce: `nonce-${myNonce}`,
        }),
      );
    } else {
      res.writeHead(400, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ code: 1, message: "bad request" }));
    }
  });
  await new Promise<void>((resolve, reject) => {
    server.listen(options.port, () => resolve());
  });
  return {
    stop() {
      server.close();
    },
    fakeVerification(nonce: string, address: Record<string, string>): void {
      const prefix = "nonce-";
      if (!nonce.startsWith(prefix)) {
        throw Error("invalid challenger nonce");
      }
      const nonceId = Number(nonce.substring(prefix.length));
      const nonceInfo = infoForNonceId.get(nonceId);
      if (!nonceInfo) {
        throw Error("nonce does not exist yet");
      }
      nonceInfo.address = address;
    },
    getSetupRequest(nonce: string): any {
      const prefix = "nonce-";
      if (!nonce.startsWith(prefix)) {
        throw Error("invalid challenger nonce");
      }
      const nonceId = Number(nonce.substring(prefix.length));
      return infoForNonceId.get(nonceId)?.setup;
    },
  };
}
