/**
 * ANCPI ePay client — server-side only.
 *
 * Reverse-engineered from epaymentAngularApp.js (123KB).
 * All endpoints and payloads verified against actual Angular source.
 *
 * Flow:
 *   1. Login (OpenAM) → AMAuthCookie
 *   2. GET ShowCartItems → Angular session context
 *   3. EpayJsonInterceptor(shoppingCartCompleteInformation) → judeteNom IDs
 *   4. EpayJsonInterceptor(nomenclatorUAT) → UAT IDs
 *   5. EpayJsonInterceptor(saveProductMetadataForBasketItem) → save (multipart)
 *   6. EditCartSubmit(goToCheckout=true) → order
 *   7. Poll ShowOrderDetails → idDocument
 *   8. DownloadFile(Content-Type: application/pdf) → PDF
 */

import axios, { type AxiosInstance } from "axios";
import crypto from "crypto";
import { wrapper } from "axios-cookiejar-support";
import { CookieJar } from "tough-cookie";
import FormData from "form-data";
import type {
  EpayCartResponse,
  EpaySearchResult,
  EpayUatEntry,
  EpayOrderStatus,
  EpaySolutionDoc,
} from "./epay-types";

/* ------------------------------------------------------------------ */
/*  Constants                                                          */
/* ------------------------------------------------------------------ */

const BASE_URL = process.env.ANCPI_BASE_URL || "https://epay.ancpi.ro/epay";
const LOGIN_URL =
  process.env.ANCPI_LOGIN_URL ||
  "https://oassl.ancpi.ro/openam/UI/Login";
const DEFAULT_TIMEOUT_MS = 60_000;
const SESSION_TTL_MS = 60 * 60 * 1000; // 1 hour
const POLL_INTERVAL_MS = 15_000;
const POLL_MAX_ATTEMPTS = 40;

/* ------------------------------------------------------------------ */
/*  Session cache                                                      */
/* ------------------------------------------------------------------ */

type SessionEntry = {
  jar: CookieJar;
  client: AxiosInstance;
  createdAt: number;
  lastUsed: number;
};

const globalStore = globalThis as {
  __epaySessionCache?: Map<string, SessionEntry>;
  __epayCleanupTimer?: ReturnType<typeof setInterval>;
};
const sessionCache =
  globalStore.__epaySessionCache ?? new Map<string, SessionEntry>();
globalStore.__epaySessionCache = sessionCache;

// Periodic cleanup of expired sessions (every 5 minutes, 9-min TTL)
if (!globalStore.__epayCleanupTimer) {
  globalStore.__epayCleanupTimer = setInterval(() => {
    const now = Date.now();
    for (const [key, entry] of sessionCache.entries()) {
      if (now - entry.lastUsed > 9 * 60_000) {
        sessionCache.delete(key);
      }
    }
  }, 5 * 60_000);
}

const makeCacheKey = (u: string, p: string) =>
  crypto.createHash("sha256").update(`epay:${u}:${p}`).digest("hex");

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

/* ------------------------------------------------------------------ */
/*  Client                                                             */
/* ------------------------------------------------------------------ */

export class EpayClient {
  private client: AxiosInstance;
  private jar: CookieJar;
  private username: string;
  private password: string;

  private constructor(
    client: AxiosInstance,
    jar: CookieJar,
    username: string,
    password: string,
  ) {
    this.client = client;
    this.jar = jar;
    this.username = username;
    this.password = password;
  }

  /* ── Factory ───────────────────────────────────────────────── */

  static async create(username: string, password: string): Promise<EpayClient> {
    const cacheKey = makeCacheKey(username, password);
    const cached = sessionCache.get(cacheKey);
    const now = Date.now();

    if (cached && now - cached.lastUsed < SESSION_TTL_MS) {
      cached.lastUsed = now;
      return new EpayClient(cached.client, cached.jar, username, password);
    }

    const jar = new CookieJar();
    const client = wrapper(
      axios.create({
        jar,
        withCredentials: true,
        maxRedirects: 10,
        headers: {
          Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
          "User-Agent":
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
        },
      }),
    );

    const instance = new EpayClient(client, jar, username, password);
    await instance.login();
    sessionCache.set(cacheKey, { jar, client, createdAt: now, lastUsed: now });
    return instance;
  }

  /* ── Auth ───────────────────────────────────────────────────── */

  private async login(): Promise<void> {
    const gotoUrl = "http://epay.ancpi.ro:80/epay/LogIn.action";
    const loginUrlFull = `${LOGIN_URL}?module=SelfRegistration&goto=${encodeURIComponent(gotoUrl)}`;

    // Step 1: GET login page (sets initial cookies)
    await this.client.get(loginUrlFull, {
      timeout: DEFAULT_TIMEOUT_MS,
      validateStatus: () => true,
    });

    // Step 2: POST credentials
    const body = `IDToken1=${encodeURIComponent(this.username)}&IDToken2=${encodeURIComponent(this.password)}`;
    const response = await this.client.post(loginUrlFull, body, {
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
        Referer: loginUrlFull,
      },
      timeout: DEFAULT_TIMEOUT_MS,
      validateStatus: () => true,
    });

    const html = typeof response.data === "string" ? response.data : "";
    if (html.includes("Authentication Failed") || html.includes("Autentificare esuata")) {
      throw new Error("ePay login failed (invalid credentials)");
    }

    // Step 3: Navigate to ePay to establish JSESSIONID
    const epayResponse = await this.client.get(gotoUrl, {
      timeout: DEFAULT_TIMEOUT_MS,
      validateStatus: () => true,
    });
    const epayHtml = String(epayResponse.data ?? "");

    if (!epayHtml.includes("puncte de credit") && !epayHtml.includes("LogOut")) {
      // Try HTTPS
      await this.client.get(`${BASE_URL}/LogIn.action`, {
        timeout: DEFAULT_TIMEOUT_MS,
        validateStatus: () => true,
      });
    }

    console.log("[epay] Login successful.");
  }

  /* ── Credits ───────────────────────────────────────────────── */

  async getCredits(): Promise<number> {
    const response = await this.client.get(`${BASE_URL}/LogIn.action`, {
      timeout: DEFAULT_TIMEOUT_MS,
    });
    const html = String(response.data ?? "");
    const match = html.match(/(\d+)\s+puncte?\s+de\s+credit/i);
    return match ? parseInt(match[1] ?? "0", 10) : 0;
  }

  /* ── Cart ───────────────────────────────────────────────────── */

  async addToCart(prodId = 14200): Promise<number> {
    const body = new URLSearchParams();
    body.set("prodId", String(prodId));
    body.set("productQtyModif", "1");
    body.set("urgencyValue", "5000");
    body.set("wishListId", "-1");
    body.set("basketRowId", "");
    body.set("addToWishList", "");
    body.set("random", String(Date.now()));
    body.set("secondaryProductsJson", "");

    const response = await this.client.post(
      `${BASE_URL}/AddToCartOrWishListFromPost.action`,
      body.toString(),
      {
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
        timeout: DEFAULT_TIMEOUT_MS,
      },
    );

    const data = response.data as EpayCartResponse;
    const item = data?.items?.[0];
    if (!item?.id) {
      throw new Error(`ePay addToCart failed: ${JSON.stringify(data).slice(0, 200)}`);
    }
    console.log(`[epay] Added to cart: basketRowId=${item.id}`);
    return item.id;
  }

  /* ── EpayJsonInterceptor (form-urlencoded) ─────────────────── */

  /**
   * Call EpayJsonInterceptor with form-urlencoded body.
   * This is how Angular's doPostAsForm works.
   */
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  private async interceptor(params: Record<string, string>): Promise<any> {
    const body = new URLSearchParams(params).toString();
    const response = await this.client.post(
      `${BASE_URL}/EpayJsonInterceptor.action`,
      body,
      {
        headers: {
          "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
          "X-Requested-With": "XMLHttpRequest",
        },
        timeout: DEFAULT_TIMEOUT_MS,
      },
    );
    const data = response.data;
    if (data?.jsonResult && typeof data.jsonResult === "string") {
      try {
        return JSON.parse(data.jsonResult);
      } catch { /* not JSON */ }
    }
    return data;
  }

  /* ── Get County IDs (internal ANCPI IDs, NOT 0-41 indices) ── */

  /**
   * Fetch shoppingCartCompleteInformation to get judeteNom
   * (internal county IDs used by all other ePay endpoints).
   */
  async getCountyList(): Promise<EpayUatEntry[]> {
    // Must load ShowCartItems first for Angular context
    await this.client.get(`${BASE_URL}/ShowCartItems.action`, {
      timeout: DEFAULT_TIMEOUT_MS,
      validateStatus: () => true,
    });

    const data = await this.interceptor({
      reqType: "shoppingCartCompleteInformation",
    });

    const judeteNom = data?.pNomenclatoareMap?.judeteNom;
    if (Array.isArray(judeteNom)) {
      console.log(`[epay] Got ${judeteNom.length} counties from judeteNom.`);
      return judeteNom as EpayUatEntry[];
    }

    console.warn("[epay] Could not get judeteNom:", JSON.stringify(data).slice(0, 300));
    return [];
  }

  /* ── UAT Lookup ────────────────────────────────────────────── */

  /**
   * Get UAT list for a county using its INTERNAL ID (from judeteNom).
   */
  async getUatList(countyInternalId: number): Promise<EpayUatEntry[]> {
    const data = await this.interceptor({
      reqType: "nomenclatorUAT",
      countyId: String(countyInternalId),
    });

    if (Array.isArray(data)) return data as EpayUatEntry[];
    console.warn(`[epay] getUatList(${countyInternalId}):`, JSON.stringify(data).slice(0, 200));
    return [];
  }

  /* ── SearchEstate (optional) ───────────────────────────────── */

  async searchEstate(
    identifier: string,
    countyInternalId: number,
    uatId: number,
  ): Promise<EpaySearchResult[]> {
    const body = new URLSearchParams();
    body.set("identificator", identifier); // NOT "identifier"!
    body.set("judet", String(countyInternalId)); // NOT "countyId"!
    body.set("uat", String(uatId)); // real UAT ID

    const response = await this.client.post(
      `${BASE_URL}/SearchEstate.action`,
      body.toString(),
      {
        headers: {
          "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
          "X-Requested-With": "XMLHttpRequest",
        },
        timeout: DEFAULT_TIMEOUT_MS,
      },
    );

    const data = response.data;
    if (Array.isArray(data)) return data as EpaySearchResult[];
    if (typeof data === "string" && data.trim().startsWith("[")) {
      try { return JSON.parse(data) as EpaySearchResult[]; } catch { /* */ }
    }
    return [];
  }

  /* ── Save Metadata (multipart/form-data) ───────────────────── */

  /**
   * Save cart item metadata via EpayJsonInterceptor with
   * reqType=saveProductMetadataForBasketItem.
   * Uses multipart/form-data with productMetadataJSON field.
   */
  async saveMetadata(
    basketRowId: number,
    countyInternalId: number,
    countyName: string,
    uatId: number,
    uatName: string,
    nrCF: string,
    nrCadastral: string,
    solicitantId: string,
  ): Promise<boolean> {
    const metadata = {
      basketId: basketRowId,
      productId: 14200,
      metadate: {
        judet: {
          name: "judet",
          bigDecimalValue: countyInternalId,
          stringValue: countyName,
          stringValues: null,
          validators: { required: "required" },
        },
        uat: {
          name: "uat",
          bigDecimalValue: uatId,
          stringValue: uatName,
          stringValues: null,
          validators: { required: "required" },
        },
        CF: {
          name: "CF",
          stringValue: null,
          stringValues: [nrCF],
          validators: { required: "required" },
        },
        CAD: {
          name: "CAD",
          stringValue: null,
          stringValues: [nrCadastral],
          validators: {},
        },
        TOPO: {
          name: "TOPO",
          stringValue: null,
          stringValues: [null],
          validators: {},
        },
        metodeLivrare: {
          name: "metodeLivrare",
          stringValue: "Electronic",
          stringValues: ["Electronic"],
        },
        differentSolicitant: {
          name: "differentSolicitant",
          stringValue: solicitantId,
          stringValues: [" "],
        },
        electronicSignedDocs: {
          name: "electronicSignedDocs",
          booleanValue: true,
        },
        enableANCPIConversation: {
          name: "enableANCPIConversation",
          booleanValue: false,
          stringValue: "false",
        },
        isMandatoryCADorTOPO: {
          name: "isMandatoryCADorTOPO",
          booleanValue: true,
        },
        singleUnit: {
          name: "singleUnit",
          booleanValue: false,
        },
        requiresReexaminare: {
          name: "requiresReexaminare",
          booleanValue: false,
        },
      },
      manufacturer: "ANCPI",
      isExternalSystem: false,
      configurationUrl: "http://www.google.com",
    };

    // Must use multipart/form-data (Angular uses doPostAsFormMultipart)
    const form = new FormData();
    form.append("reqType", "saveProductMetadataForBasketItem");
    form.append("productMetadataJSON", JSON.stringify(metadata));

    const response = await this.client.post(
      `${BASE_URL}/EpayJsonInterceptor.action`,
      form,
      {
        headers: {
          ...form.getHeaders(),
          "X-Requested-With": "XMLHttpRequest",
        },
        timeout: DEFAULT_TIMEOUT_MS,
      },
    );

    const data = response.data;
    const result =
      typeof data?.jsonResult === "string"
        ? JSON.parse(data.jsonResult)
        : data;

    if (result?.code === "SUCCESS" || result?.code === "SAVE_OK") {
      console.log(`[epay] Metadata saved for basket ${basketRowId}.`);
      return true;
    }

    console.error("[epay] saveMetadata failed:", JSON.stringify(result).slice(0, 300));
    return false;
  }

  /* ── Order Submission ──────────────────────────────────────── */

  /**
   * Submit order. Pass knownOrderIds to skip them when finding the new orderId.
   */
  async submitOrder(knownOrderIds?: Set<string>): Promise<string> {
    // Get current latest orderId BEFORE submitting (to detect the new one)
    let previousOrderId: string | null = null;
    try {
      previousOrderId = await this.getLatestOrderId();
    } catch {
      // No previous orders
    }

    // Step 1: POST EditCartSubmit (Angular does this, gets 200 back)
    const body = new URLSearchParams();
    body.set("goToCheckout", "true");

    await this.client.post(
      `${BASE_URL}/EditCartSubmit.action`,
      body.toString(),
      {
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
        timeout: DEFAULT_TIMEOUT_MS,
        validateStatus: () => true,
      },
    );

    // Step 2: GET CheckoutConfirmationSubmit (Angular redirects here client-side)
    const confirmResponse = await this.client.get(
      `${BASE_URL}/CheckoutConfirmationSubmit.action`,
      {
        timeout: DEFAULT_TIMEOUT_MS,
        maxRedirects: 5,
        validateStatus: () => true,
      },
    );

    const confirmHtml = String(confirmResponse.data ?? "");
    const hasConfirmation =
      confirmHtml.includes("mulțumim") ||
      confirmHtml.includes("multumim") ||
      confirmHtml.includes("confirmare");

    console.log(
      `[epay] Checkout confirmation: ${hasConfirmation ? "OK" : "uncertain"} (${confirmHtml.length} chars)`,
    );

    // Find the NEW orderId (different from previous + not in knownOrderIds)
    return this.findNewOrderId(previousOrderId, knownOrderIds);
  }

  private async getLatestOrderId(): Promise<string> {
    const response = await this.client.get(`${BASE_URL}/LogIn.action`, {
      timeout: DEFAULT_TIMEOUT_MS,
    });
    const html = String(response.data ?? "");
    const match = html.match(/ShowOrderDetails\.action\?orderId=(\d+)/);
    if (match) return match[1] ?? "";
    throw new Error("Could not determine orderId");
  }

  private async findNewOrderId(
    previousOrderId: string | null,
    knownOrderIds?: Set<string>,
  ): Promise<string> {
    const response = await this.client.get(`${BASE_URL}/LogIn.action`, {
      timeout: DEFAULT_TIMEOUT_MS,
    });
    const html = String(response.data ?? "");

    // Find ALL orderIds on the page
    const allMatches = html.matchAll(/ShowOrderDetails\.action\?orderId=(\d+)/g);
    for (const m of allMatches) {
      const oid = m[1] ?? "";
      if (!oid) continue;
      if (oid === previousOrderId) continue;
      if (knownOrderIds?.has(oid)) continue;
      console.log(`[epay] New orderId: ${oid}`);
      return oid;
    }

    // If no new orderId found, the latest one might be it (first order)
    const latest = html.match(/ShowOrderDetails\.action\?orderId=(\d+)/);
    if (latest?.[1]) {
      console.log(`[epay] Using latest orderId: ${latest[1]}`);
      return latest[1];
    }

    throw new Error("Could not determine orderId after checkout");
  }

  /* ── Order Status & Polling ────────────────────────────────── */

  async getOrderStatus(orderId: string): Promise<EpayOrderStatus> {
    const response = await this.client.get(
      `${BASE_URL}/ShowOrderDetails.action?orderId=${orderId}`,
      { timeout: DEFAULT_TIMEOUT_MS },
    );
    const html = String(response.data ?? "");

    let status = "Receptionata";
    if (html.includes("Finalizata")) status = "Finalizata";
    else if (html.includes("In curs de procesare")) status = "In curs de procesare";
    else if (html.includes("Anulata")) status = "Anulata";
    else if (html.includes("Plata refuzata")) status = "Plata refuzata";

    const documents: EpaySolutionDoc[] = [];
    // Map CF number → document (for correct batch matching)
    const documentsByCadastral = new Map<string, EpaySolutionDoc>();

    // Decode HTML entities — ePay embeds JSON with &quot; encoding
    const decoded = html
      .replace(/&quot;/g, '"')
      .replace(/&amp;/g, "&")
      .replace(/&lt;/g, "<")
      .replace(/&gt;/g, ">")
      .replace(/&#x21B;/g, "ț")
      .replace(/&#x219;/g, "ș");

    // Strategy: find CF numbers and solutii separately, then match by position.
    // In the HTML, each metadateCereri block has both CF.stringValues and solutii
    // in the same order. We find them independently and zip them.

    // 1. Find all CF numbers: "CF":{"name":"CF",...,"stringValues":["345295"]}
    const cfPattern = /"CF"\s*:\s*\{[^}]*"stringValues"\s*:\s*\["(\d+)"\]/g;
    const cfNumbers: string[] = [];
    let cfMatch;
    while ((cfMatch = cfPattern.exec(decoded)) !== null) {
      cfNumbers.push(cfMatch[1] ?? "");
    }

    // 2. Find all solutii blocks (CF extracts only, idTipDocument=null)
    const solutiiPattern = /"solutii"\s*:\s*\[(\{[^[\]]*\})\]/g;
    const allSolutii: EpaySolutionDoc[] = [];
    let solutiiMatch;
    while ((solutiiMatch = solutiiPattern.exec(decoded)) !== null) {
      try {
        const arr = JSON.parse(`[${solutiiMatch[1]}]`);
        for (const s of arr) {
          if (s?.idDocument && s?.idTipDocument === null) {
            allSolutii.push({
              idDocument: s.idDocument,
              idTipDocument: null,
              nume: s.nume ?? "",
              numar: s.numar ?? null,
              serie: s.serie ?? null,
              dataDocument: s.dataDocument ?? "",
              contentType: s.contentType ?? "application/pdf",
              linkDownload: s.linkDownload ?? "",
              downloadValabil: s.downloadValabil ?? true,
              valabilNelimitat: s.valabilNelimitat ?? true,
              zileValabilitateDownload: s.zileValabilitateDownload ?? -1,
              transactionId: s.transactionId ?? 0,
            });
          }
        }
      } catch { /* parse failed */ }
    }

    // 3. Zip CF numbers with solutii — they appear in the same order
    documents.push(...allSolutii);
    for (let i = 0; i < Math.min(cfNumbers.length, allSolutii.length); i++) {
      const cf = cfNumbers[i];
      const doc = allSolutii[i];
      if (cf && doc) {
        documentsByCadastral.set(cf, doc);
      }
    }

    console.log(
      `[epay] Order ${orderId}: CFs=[${cfNumbers.join(",")}], docs=${allSolutii.length}, matched=${documentsByCadastral.size}`,
    );

    if (documents.length === 0 && status === "Finalizata") {
      console.warn(`[epay] Order ${orderId}: Finalizata but no documents found`);
    }

    console.log(
      `[epay] Order ${orderId}: ${documents.length} docs, ${documentsByCadastral.size} matched by CF`,
    );

    return { orderId, status, documents, documentsByCadastral };
  }

  async pollUntilComplete(
    orderId: string,
    onProgress?: (attempt: number, status: string) => void,
  ): Promise<EpayOrderStatus> {
    for (let attempt = 1; attempt <= POLL_MAX_ATTEMPTS; attempt++) {
      const status = await this.getOrderStatus(orderId);
      if (onProgress) onProgress(attempt, status.status);
      if (["Finalizata", "Anulata", "Plata refuzata"].includes(status.status)) {
        return status;
      }
      await sleep(POLL_INTERVAL_MS);
    }
    throw new Error(`ePay order ${orderId} timed out after ${POLL_MAX_ATTEMPTS} poll attempts`);
  }

  /* ── Document Download ─────────────────────────────────────── */

  async downloadDocument(idDocument: number, typeD = 4): Promise<Buffer> {
    const url = `${BASE_URL}/DownloadFile.action?typeD=${typeD}&id=${idDocument}&source=&browser=chrome`;
    // Angular sends Content-Type: application/pdf in the REQUEST
    const response = await this.client.post(url, null, {
      headers: { "Content-Type": "application/pdf" },
      timeout: DEFAULT_TIMEOUT_MS,
      responseType: "arraybuffer",
    });

    const data = response.data;
    if (!data || data.length < 100) {
      throw new Error(`ePay download empty (${data?.length ?? 0} bytes)`);
    }
    console.log(`[epay] Downloaded document ${idDocument}: ${data.length} bytes`);
    return Buffer.from(data);
  }

  /* ── Utility ───────────────────────────────────────────────── */

  async getRawHtml(url: string): Promise<string> {
    const response = await this.client.get(url, {
      timeout: DEFAULT_TIMEOUT_MS,
      validateStatus: () => true,
    });
    return String(response.data ?? "");
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  async postRaw(url: string, body: string, extraHeaders?: Record<string, string>): Promise<any> {
    const response = await this.client.post(url, body, {
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
        ...extraHeaders,
      },
      timeout: DEFAULT_TIMEOUT_MS,
      validateStatus: () => true,
    });
    return response.data;
  }
}
