import crypto from "crypto";
import { nodeEnv } from "./env";

const SANDBOX_CHECKOUT = "https://sandbox.ipay.lk/ipg/checkout";
const LIVE_CHECKOUT = "https://ipay.lk/ipg/checkout";

export type CallbackPayload = {
  transactionReference: string;
  orderId: string;
  transactionTimeInMillis: string;
  transactionAmount: string;
  transactionStatus: string;
  transactionMessage: string;
  merchantParam1: string;
  merchantParam2: string;
  checksum: string;
  creditedAmount: string;
};

export function ipayToken(): string {
  return (process.env.IPAY_MERCHANT_WEB_TOKEN || "").trim();
}

export function ipaySecret(): string {
  return (process.env.IPAY_SECRET || "").trim();
}

export function ipayConfigured(): boolean {
  return Boolean(ipayToken() && ipaySecret());
}

export function ipayCheckoutUrl(): string {
  const configured = (process.env.IPAY_CHECKOUT_URL || "").trim();
  if (configured) return configured;
  return nodeEnv() === "production" ? LIVE_CHECKOUT : SANDBOX_CHECKOUT;
}

export function checkoutOrigins(): string[] {
  const origins = new Set<string>([new URL(SANDBOX_CHECKOUT).origin, new URL(LIVE_CHECKOUT).origin]);
  try {
    origins.add(new URL(ipayCheckoutUrl()).origin);
  } catch {
    // Ignore a malformed override; checkout itself will fail later.
  }
  return [...origins];
}

/**
 * Nested HMAC-SHA256, then Base64.
 * checksum = Base64( HMAC(secret, HMAC(secret, message)) )
 * The inner digest is raw bytes, not a hex or Base64 string.
 */
export function nestedHmacBase64(secret: string, message: string): string {
  const inner = crypto.createHmac("sha256", secret).update(message, "utf8").digest();
  return crypto.createHmac("sha256", secret).update(inner).digest("base64");
}

export function checkoutChecksum(secret: string, token: string, orderId: string, totalAmount: string): string {
  return nestedHmacBase64(secret, `${token}${orderId}${totalAmount}`);
}

export function callbackMessage(payload: Pick<
  CallbackPayload,
  "transactionReference" | "orderId" | "transactionTimeInMillis" | "transactionAmount" | "transactionStatus"
>): string {
  return (
    payload.transactionReference +
    payload.orderId +
    payload.transactionTimeInMillis +
    payload.transactionAmount +
    payload.transactionStatus
  );
}

export function callbackChecksum(secret: string, payload: CallbackPayload): string {
  return nestedHmacBase64(secret, callbackMessage(payload));
}

export function webhookSignature(secret: string, rawBody: string): string {
  return crypto.createHmac("sha256", secret).update(rawBody, "utf8").digest("hex");
}

export function safeEqual(left: string, right: string): boolean {
  const a = Buffer.from(left);
  const b = Buffer.from(right);
  if (a.length !== b.length) {
    crypto.timingSafeEqual(a, a);
    return false;
  }
  return crypto.timingSafeEqual(a, b);
}

export function normalizeChecksum(value: string): string {
  // Form bodies decode "+" as a space. Base64 checksums use "+".
  return value.trim().replace(/ /g, "+");
}

export function checkoutFormFields(input: {
  token: string;
  secret: string;
  orderId: string;
  amount: string;
  description: string;
  hubReturnUrl: string;
  hubCancelUrl: string;
  customerName: string;
  customerEmail: string;
  customerPhone: string;
  productId: string;
  paymentId: string;
}): Record<string, string> {
  const fields: Record<string, string> = {
    merchantWebToken: input.token,
    totalAmount: input.amount,
    orderId: input.orderId,
  };
  if (input.description) fields.orderDescription = input.description;
  fields.returnUrl = input.hubReturnUrl;
  fields.cancelUrl = input.hubCancelUrl;
  if (input.customerName) fields.customerName = input.customerName;
  if (input.customerPhone) fields.customerPhone = input.customerPhone;
  if (input.customerEmail) fields.customerEmail = input.customerEmail;
  fields.merchantParam1 = input.productId;
  fields.merchantParam2 = input.paymentId;
  fields.checksum = checkoutChecksum(input.secret, input.token, input.orderId, input.amount);
  return fields;
}

function asText(value: unknown): string {
  if (value === undefined || value === null) return "";
  if (Array.isArray(value)) return asText(value[0]);
  return String(value);
}

function extractJsonLiteral(raw: string, key: string): string | undefined {
  const stringMatch = raw.match(new RegExp(`"${key}"\\s*:\\s*"((?:\\\\.|[^"\\\\])*)"`));
  if (stringMatch?.[1] !== undefined) {
    try {
      return JSON.parse(`"${stringMatch[1]}"`) as string;
    } catch {
      return stringMatch[1];
    }
  }
  const numberMatch = raw.match(new RegExp(`"${key}"\\s*:\\s*(-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?)`));
  if (numberMatch?.[1]) return numberMatch[1];
  return undefined;
}

export function parseCallbackBody(contentType: string, rawBody: string, parsed: unknown): CallbackPayload {
  const type = contentType.toLowerCase();
  const raw = rawBody || "";
  let body: Record<string, unknown> = {};

  if (parsed && typeof parsed === "object" && !Buffer.isBuffer(parsed) && !Array.isArray(parsed)) {
    body = parsed as Record<string, unknown>;
  }

  const looksJson = type.includes("json") || raw.trim().startsWith("{");
  if (looksJson && raw.trim().startsWith("{") && Object.keys(body).length === 0) {
    const decoded = JSON.parse(raw) as unknown;
    if (!decoded || typeof decoded !== "object" || Array.isArray(decoded)) {
      throw new Error("Callback JSON must be an object");
    }
    body = decoded as Record<string, unknown>;
  }

  const read = (key: string): string => {
    if (looksJson && raw.trim().startsWith("{")) {
      const literal = extractJsonLiteral(raw, key);
      if (literal !== undefined) return literal;
    }
    return asText(body[key]);
  };

  return {
    transactionReference: read("transactionReference"),
    orderId: read("orderId"),
    transactionTimeInMillis: read("transactionTimeInMillis"),
    transactionAmount: read("transactionAmount"),
    transactionStatus: read("transactionStatus").trim(),
    transactionMessage: read("transactionMessage"),
    merchantParam1: read("merchantParam1"),
    merchantParam2: read("merchantParam2"),
    checksum: normalizeChecksum(read("checksum")),
    creditedAmount: read("creditedAmount"),
  };
}
