import * as kv from "./kv_store.tsx";
import type { PlanoId } from "./modulos.tsx";

export type MpesaCheckoutStatus =
  | "PENDING"
  | "PAID"
  | "FAILED"
  | "CANCELLED"
  | "TIMEOUT"
  | "DUPLICATE"
  | "CONFIG_ERROR"
  | "UNKNOWN";

export type MpesaConfig = {
  env: "sandbox" | "production";
  host: string;
  port: string;
  path: string;
  origin: string;
  apiKey?: string;
  publicKey?: string;
  accessToken?: string;
  serviceProviderCode: string;
};

export type MpesaC2BResponse = {
  output_ConversationID?: string;
  output_TransactionID?: string;
  output_ResponseDesc?: string;
  output_ResponseCode?: string;
  output_ThirdPartyReference?: string;
  output_error?: string;
  error?: string;
  message?: string;
  raw?: string;
  [key: string]: unknown;
};

export type MpesaCheckout = {
  id: string;
  tipo: "subscricao";
  status: MpesaCheckoutStatus;
  planoId: PlanoId;
  planoNome: string;
  nomeComercial: string;
  nuit: string;
  emailAdmin: string;
  telefoneMpesa: string;
  msisdn: string;
  valorPlano: number;
  taxaLicenca: number;
  valorTotal: number;
  cicloMeses: number;
  transactionReference: string;
  thirdPartyReference: string;
  serviceProviderCode: string;
  conversationId?: string;
  transactionId?: string;
  responseCode?: string;
  responseDescription?: string;
  httpStatus?: number;
  requestPayload?: Record<string, string>;
  responsePayload?: MpesaC2BResponse | Record<string, unknown>;
  error?: string;
  empresaId?: string;
  usuarioId?: string;
  credenciaisGeradas?: {
    email: string;
    senhaTemporaria: string;
  };
  dataCriacao: string;
  dataUltimaAtualizacao: string;
};

export type CriarCheckoutInput = {
  planoId: PlanoId;
  planoNome: string;
  nomeComercial: string;
  nuit: string;
  emailAdmin: string;
  telefoneMpesa: string;
  valorPlano: number;
  taxaLicenca: number;
  cicloMeses: number;
};

const CHECKOUT_PREFIX = "mpesa_checkout:";
const CHECKOUT_REF_PREFIX = "mpesa_checkout_ref:";
const SANDBOX_SERVICE_PROVIDER_CODE = "171717";
const PRODUCTION_SERVICE_PROVIDER_CODE = "903432";
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;

class MpesaConfigurationError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "MpesaConfigurationError";
  }
}

function env(name: string, fallback = ""): string {
  try {
    return (Deno.env.get(name) || fallback).trim();
  } catch {
    return fallback;
  }
}

// Em producao, cada credencial tenta primeiro a variante MPESA_PRODUCTION_*
// e só recorre à variante genérica (MPESA_*) como compatibilidade - assim
// sandbox e producao podem ficar guardadas lado a lado no mesmo .env e o
// unico interruptor real é MPESA_ENV, sem nunca copiar chaves à mão entre
// as duas (risco de ficar a chave errada activa sem se notar).
function envProducao(nomeProducao: string, nomeGenerico: string, fallback = ""): string {
  return env(nomeProducao) || env(nomeGenerico, fallback);
}

export function obterConfig(): MpesaConfig {
  const modo = env("MPESA_ENV", "sandbox").toLowerCase() === "production" ? "production" : "sandbox";
  const emProducao = modo === "production";

  return {
    env: modo,
    host: emProducao
      ? envProducao("MPESA_PRODUCTION_HOST", "MPESA_HOST")
      : env("MPESA_HOST", "api.sandbox.vm.co.mz"),
    port: emProducao ? envProducao("MPESA_PRODUCTION_PORT", "MPESA_PORT", "18352") : env("MPESA_PORT", "18352"),
    path: emProducao
      ? envProducao("MPESA_PRODUCTION_C2B_PATH", "MPESA_C2B_PATH", "/ipg/v1x/c2bPayment/singleStage/")
      : env("MPESA_C2B_PATH", "/ipg/v1x/c2bPayment/singleStage/"),
    origin: emProducao
      ? envProducao("MPESA_PRODUCTION_ORIGIN", "MPESA_ORIGIN", "developer.mpesa.vm.co.mz")
      : env("MPESA_ORIGIN", "developer.mpesa.vm.co.mz"),
    apiKey: emProducao ? envProducao("MPESA_PRODUCTION_API_KEY", "MPESA_API_KEY") : env("MPESA_API_KEY"),
    publicKey: emProducao ? envProducao("MPESA_PRODUCTION_PUBLIC_KEY", "MPESA_PUBLIC_KEY") : env("MPESA_PUBLIC_KEY"),
    accessToken: emProducao
      ? envProducao("MPESA_PRODUCTION_ACCESS_TOKEN", "MPESA_ACCESS_TOKEN")
      : env("MPESA_ACCESS_TOKEN"),
    serviceProviderCode:
      (emProducao
        ? envProducao("MPESA_PRODUCTION_SERVICE_PROVIDER_CODE", "MPESA_SERVICE_PROVIDER_CODE")
        : env("MPESA_SERVICE_PROVIDER_CODE")) ||
      env("MPESA_SHORTCODE") ||
      (modo === "sandbox" ? SANDBOX_SERVICE_PROVIDER_CODE : PRODUCTION_SERVICE_PROVIDER_CODE),
  };
}

export function validarConfig(config = obterConfig()): { ok: boolean; error?: string } {
  if (!config.host) {
    return { ok: false, error: "MPESA_HOST nao esta configurado" };
  }

  if (!config.serviceProviderCode) {
    return { ok: false, error: "MPESA_SERVICE_PROVIDER_CODE ou MPESA_SHORTCODE nao esta configurado" };
  }

  if (!config.accessToken && (!config.apiKey || !config.publicKey)) {
    return {
      ok: false,
      error: "Configure MPESA_API_KEY e MPESA_PUBLIC_KEY, ou defina MPESA_ACCESS_TOKEN ja encriptado",
    };
  }

  return { ok: true };
}

function publicKeyToPem(publicKey: string): string {
  if (publicKey.includes("BEGIN PUBLIC KEY")) {
    return publicKey;
  }

  const clean = publicKey.replace(/\s+/g, "");
  const body = clean.match(/.{1,64}/g)?.join("\n") || clean;
  return `-----BEGIN PUBLIC KEY-----\n${body}\n-----END PUBLIC KEY-----`;
}

async function gerarBearerToken(config: MpesaConfig): Promise<string> {
  if (config.accessToken) {
    return config.accessToken;
  }

  if (!config.apiKey || !config.publicKey) {
    throw new Error("Credenciais M-Pesa incompletas");
  }

  try {
    const [{ publicEncrypt, constants }, { Buffer }] = await Promise.all([
      import("node:crypto"),
      import("node:buffer"),
    ]);

    const encrypted = publicEncrypt(
      {
        key: publicKeyToPem(config.publicKey),
        padding: constants.RSA_PKCS1_PADDING,
      },
      Buffer.from(config.apiKey, "utf8")
    );

    return encrypted.toString("base64");
  } catch (error: any) {
    throw new MpesaConfigurationError(
      `Nao foi possivel encriptar a API Key M-Pesa neste runtime. Configure MPESA_ACCESS_TOKEN. Detalhe: ${error?.message || error}`
    );
  }
}

export function normalizarTelefoneMpesa(telefone: string): string {
  const digits = String(telefone || "").replace(/\D/g, "");

  if (digits.startsWith("258") && /^2588[45]\d{7}$/.test(digits)) {
    return digits;
  }

  if (/^8[45]\d{7}$/.test(digits)) {
    return `258${digits}`;
  }

  throw new Error("Telefone M-Pesa invalido. Use um numero Vodacom iniciado por 84 ou 85");
}

function gerarReferencia(prefixo: string): string {
  const base = `${prefixo}${Date.now().toString(36)}${Math.random().toString(36).slice(2, 7)}`;
  return base.toUpperCase().replace(/[^A-Z0-9]/g, "").slice(0, 20);
}

function formatarValorMpesa(valor: number): string {
  if (!Number.isFinite(valor) || valor <= 0) {
    throw new Error("Valor M-Pesa invalido");
  }

  return Number.isInteger(valor) ? String(valor) : valor.toFixed(2);
}

function mapearStatus(httpStatus: number, responseCode?: string, data?: MpesaC2BResponse): MpesaCheckoutStatus {
  if (httpStatus === 401 || httpStatus === 403) return "CONFIG_ERROR";
  if (httpStatus === 408) return "TIMEOUT";
  if (httpStatus === 409) return "DUPLICATE";

  switch (responseCode) {
    case "INS-0":
      // output_TransactionID e o identificador real e definitivo da
      // Vodacom para a transacao - so isso prova o pagamento. Ate 22/06/2026
      // o output_ConversationID vinha sempre preenchido tambem e era exigido
      // aqui, mas a Vodacom passou a devolve-lo vazio em INS-0 legitimos
      // (confirmado com pedidos reais em 03/08/2026) - exigi-lo deixava
      // pagamentos genuinamente confirmados presos em PENDING para sempre.
      return data?.output_TransactionID ? "PAID" : "PENDING";
    case "INS-5":
      return "CANCELLED";
    case "INS-9":
      return "TIMEOUT";
    case "INS-10":
      return "DUPLICATE";
    case undefined:
    case "":
      return httpStatus >= 400 && httpStatus < 500 ? "FAILED" : "UNKNOWN";
    default:
      return "FAILED";
  }
}

function pareceHtml(valor: string): boolean {
  const normalizado = valor.trim().toLowerCase();
  return normalizado.startsWith("<!doctype html") || normalizado.startsWith("<html") ||
    normalizado.includes("<body") || normalizado.includes("</html>");
}

function obterTimeoutRequisicaoMs(): number {
  const configurado = Number(env("MPESA_REQUEST_TIMEOUT_MS", String(DEFAULT_REQUEST_TIMEOUT_MS)));
  return Number.isFinite(configurado) && configurado > 0
    ? Math.floor(configurado)
    : DEFAULT_REQUEST_TIMEOUT_MS;
}

export function checkoutMpesaTemResultadoAmbiguo(checkout?: MpesaCheckout): boolean {
  if (!checkout) return false;

  if (["PENDING", "TIMEOUT", "UNKNOWN", "DUPLICATE"].includes(checkout.status)) {
    return true;
  }

  if (checkout.httpStatus && [502, 503, 504].includes(checkout.httpStatus)) {
    return true;
  }

  const raw = checkout.responsePayload?.raw;
  return typeof raw === "string" && pareceHtml(raw);
}

export function obterDescricaoErroMpesa(
  httpStatus: number,
  data: MpesaC2BResponse | Record<string, unknown>,
): string {
  if ([502, 503, 504].includes(httpStatus)) {
    return `Servico M-Pesa temporariamente indisponivel (HTTP ${httpStatus}). O resultado do pagamento esta pendente; aguarde antes de tentar novamente.`;
  }

  if (httpStatus === 401 || httpStatus === 403) {
    return "As credenciais M-Pesa foram recusadas. Contacte o suporte para verificar a configuracao do pagamento.";
  }

  const campos = [
    "output_ResponseDesc",
    "output_error",
    "error",
    "message",
    "raw",
  ];

  for (const campo of campos) {
    const valor = data?.[campo];
    if (typeof valor === "string" && valor.trim()) {
      return pareceHtml(valor)
        ? `O gateway M-Pesa devolveu uma resposta invalida (HTTP ${httpStatus}). O resultado do pagamento esta pendente; aguarde antes de tentar novamente.`
        : valor.trim();
    }
  }

  return `Pagamento M-Pesa nao confirmado (${httpStatus})`;
}

async function guardarCheckout(checkout: MpesaCheckout): Promise<void> {
  await kv.set(`${CHECKOUT_PREFIX}${checkout.id}`, checkout);
  await kv.set(`${CHECKOUT_REF_PREFIX}${checkout.thirdPartyReference}`, checkout.id);
}

export async function obterCheckout(idOuReferencia: string): Promise<MpesaCheckout | null> {
  const direto = await kv.get(`${CHECKOUT_PREFIX}${idOuReferencia}`);
  if (direto) {
    return direto as MpesaCheckout;
  }

  const id = await kv.get(`${CHECKOUT_REF_PREFIX}${idOuReferencia}`);
  if (!id) {
    return null;
  }

  const checkout = await kv.get(`${CHECKOUT_PREFIX}${id}`);
  return (checkout as MpesaCheckout) || null;
}

async function chamarMpesaAPI(payload: Record<string, string>, config: MpesaConfig): Promise<{
  httpStatus: number;
  data: MpesaC2BResponse | Record<string, unknown>;
}> {
  const bearer = await gerarBearerToken(config);
  const url = `https://${config.host}:${config.port}${config.path}`;

  const response = await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${bearer}`,
      Origin: config.origin,
    },
    body: JSON.stringify(payload),
    signal: AbortSignal.timeout(obterTimeoutRequisicaoMs()),
  });

  let data: MpesaC2BResponse | Record<string, unknown> = {};
  const text = await response.text();

  if (text) {
    try {
      data = JSON.parse(text);
    } catch {
      data = { raw: text };
    }
  }

  return {
    httpStatus: response.status,
    data,
  };
}

export async function criarCheckoutC2B(input: CriarCheckoutInput): Promise<{
  success: boolean;
  checkout?: MpesaCheckout;
  error?: string;
}> {
  const agora = new Date().toISOString();
  const config = obterConfig();
  const validacao = validarConfig(config);
  const valorTotal = input.taxaLicenca + input.valorPlano;
  const id = `mpesa_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
  const msisdn = normalizarTelefoneMpesa(input.telefoneMpesa);
  const transactionReference = gerarReferencia("FT");
  const thirdPartyReference = gerarReferencia("TP");

  const requestPayload = {
    input_TransactionReference: transactionReference,
    input_CustomerMSISDN: msisdn,
    input_Amount: formatarValorMpesa(valorTotal),
    input_ThirdPartyReference: thirdPartyReference,
    input_ServiceProviderCode: config.serviceProviderCode,
  };

  let checkout: MpesaCheckout = {
    id,
    tipo: "subscricao",
    status: validacao.ok ? "PENDING" : "CONFIG_ERROR",
    planoId: input.planoId,
    planoNome: input.planoNome,
    nomeComercial: input.nomeComercial,
    nuit: input.nuit,
    emailAdmin: input.emailAdmin,
    telefoneMpesa: input.telefoneMpesa,
    msisdn,
    valorPlano: input.valorPlano,
    taxaLicenca: input.taxaLicenca,
    valorTotal,
    cicloMeses: input.cicloMeses,
    transactionReference,
    thirdPartyReference,
    serviceProviderCode: config.serviceProviderCode,
    requestPayload,
    error: validacao.error,
    dataCriacao: agora,
    dataUltimaAtualizacao: agora,
  };

  await guardarCheckout(checkout);

  if (!validacao.ok) {
    return { success: false, checkout, error: validacao.error };
  }

  try {
    const resposta = await chamarMpesaAPI(requestPayload, config);
    const data = resposta.data as MpesaC2BResponse;
    const responseCode = data.output_ResponseCode;
    const status = mapearStatus(resposta.httpStatus, responseCode, data);
    const statusError =
      status === "PENDING"
        ? "Pagamento M-Pesa ainda sem identificadores finais de confirmacao"
        : obterDescricaoErroMpesa(resposta.httpStatus, data);

    checkout = {
      ...checkout,
      status,
      httpStatus: resposta.httpStatus,
      conversationId: data.output_ConversationID,
      transactionId: data.output_TransactionID,
      responseCode,
      responseDescription: data.output_ResponseDesc,
      responsePayload: data,
      error: status === "PAID" ? undefined : statusError,
      dataUltimaAtualizacao: new Date().toISOString(),
    };

    await guardarCheckout(checkout);

    return {
      success: status === "PAID",
      checkout,
      error: status === "PAID" ? undefined : checkout.error,
    };
  } catch (error: any) {
    const erroConfiguracao = error?.name === "MpesaConfigurationError";
    const timeout = error?.name === "TimeoutError" || error?.name === "AbortError";
    checkout = {
      ...checkout,
      status: erroConfiguracao ? "CONFIG_ERROR" : timeout ? "TIMEOUT" : "UNKNOWN",
      error: erroConfiguracao
        ? error?.message || String(error)
        : timeout
          ? "O pedido ao M-Pesa excedeu o tempo limite. O resultado do pagamento esta pendente; aguarde antes de tentar novamente."
          : "Nao foi possivel confirmar a resposta do M-Pesa. O resultado do pagamento esta pendente; aguarde antes de tentar novamente.",
      dataUltimaAtualizacao: new Date().toISOString(),
    };

    await guardarCheckout(checkout);

    return { success: false, checkout, error: checkout.error };
  }
}

// ========== B2C (Business to Customer) - levantamento de comissoes ==========
// Move fundos da carteira M-Pesa da Tonline para o numero M-Pesa de um
// revendedor. Endpoint, porta e path proprios (diferentes do C2B) - por
// omissao usa a mesma api key/public key/service provider code do C2B como
// fallback, mas pode ser configurado em separado (MPESA_B2C_*) caso a
// Vodacom emita credenciais distintas para o produto B2C/Disbursement.

export type MpesaB2CConfig = {
  host: string;
  port: string;
  path: string;
  origin: string;
  apiKey?: string;
  publicKey?: string;
  accessToken?: string;
  serviceProviderCode: string;
};

export function obterConfigB2C(): MpesaB2CConfig {
  const configC2B = obterConfig();

  return {
    host: env("MPESA_B2C_HOST", configC2B.host),
    port: env("MPESA_B2C_PORT", "18345"),
    path: env("MPESA_B2C_PATH", "/ipg/v1x/b2cPayment/"),
    origin: env("MPESA_B2C_ORIGIN", configC2B.origin),
    apiKey: env("MPESA_B2C_API_KEY", configC2B.apiKey),
    publicKey: env("MPESA_B2C_PUBLIC_KEY", configC2B.publicKey),
    accessToken: env("MPESA_B2C_ACCESS_TOKEN", configC2B.accessToken),
    serviceProviderCode: env("MPESA_B2C_SERVICE_PROVIDER_CODE", configC2B.serviceProviderCode),
  };
}

export function validarConfigB2C(config = obterConfigB2C()): { ok: boolean; error?: string } {
  if (!config.host) {
    return { ok: false, error: "MPESA_B2C_HOST nao esta configurado" };
  }

  if (!config.serviceProviderCode) {
    return { ok: false, error: "MPESA_B2C_SERVICE_PROVIDER_CODE nao esta configurado" };
  }

  if (!config.accessToken && (!config.apiKey || !config.publicKey)) {
    return {
      ok: false,
      error: "Configure MPESA_B2C_API_KEY e MPESA_B2C_PUBLIC_KEY, ou defina MPESA_B2C_ACCESS_TOKEN ja encriptado",
    };
  }

  return { ok: true };
}

export type CriarPagamentoB2CInput = {
  numeroMpesa: string;
  valor: number;
};

export type ResultadoPagamentoB2C = {
  success: boolean;
  httpStatus?: number;
  conversationId?: string;
  transactionId?: string;
  responseCode?: string;
  responseDescription?: string;
  transactionReference: string;
  thirdPartyReference: string;
  error?: string;
  saldoInsuficienteTonline?: boolean;
};

export async function criarPagamentoB2C(input: CriarPagamentoB2CInput): Promise<ResultadoPagamentoB2C> {
  const config = obterConfigB2C();
  const validacao = validarConfigB2C(config);
  const transactionReference = gerarReferencia("RV");
  const thirdPartyReference = gerarReferencia("TP");

  if (!validacao.ok) {
    return { success: false, error: validacao.error, transactionReference, thirdPartyReference };
  }

  let msisdn: string;
  try {
    msisdn = normalizarTelefoneMpesa(input.numeroMpesa);
  } catch (error: any) {
    return { success: false, error: error?.message || "Numero M-Pesa invalido", transactionReference, thirdPartyReference };
  }

  const requestPayload = {
    input_TransactionReference: transactionReference,
    input_CustomerMSISDN: msisdn,
    input_Amount: formatarValorMpesa(input.valor),
    input_ThirdPartyReference: thirdPartyReference,
    input_ServiceProviderCode: config.serviceProviderCode,
  };

  try {
    const resposta = await chamarMpesaAPI(requestPayload, config as MpesaConfig);
    const data = resposta.data as MpesaC2BResponse;
    const responseCode = data.output_ResponseCode;
    const sucesso = responseCode === "INS-0" && Boolean(data.output_TransactionID);
    const saldoInsuficienteTonline = responseCode === "INS-2006";

    return {
      success: sucesso,
      httpStatus: resposta.httpStatus,
      conversationId: data.output_ConversationID,
      transactionId: data.output_TransactionID,
      responseCode,
      responseDescription: data.output_ResponseDesc,
      transactionReference,
      thirdPartyReference,
      error: sucesso
        ? undefined
        : saldoInsuficienteTonline
          ? "Saldo insuficiente na carteira M-Pesa da Tonline. Contacte o suporte para regularizar."
          : obterDescricaoErroMpesa(resposta.httpStatus, data),
      saldoInsuficienteTonline,
    };
  } catch (error: any) {
    return {
      success: false,
      error: error?.message || String(error),
      transactionReference,
      thirdPartyReference,
    };
  }
}

export async function marcarProvisionado(
  checkoutId: string,
  dados: {
    empresaId: string;
    usuarioId?: string;
    credenciaisGeradas?: { email: string; senhaTemporaria: string };
  }
): Promise<MpesaCheckout | null> {
  const checkout = await obterCheckout(checkoutId);
  if (!checkout) {
    return null;
  }

  const atualizado: MpesaCheckout = {
    ...checkout,
    empresaId: dados.empresaId,
    usuarioId: dados.usuarioId,
    credenciaisGeradas: dados.credenciaisGeradas,
    dataUltimaAtualizacao: new Date().toISOString(),
  };

  await guardarCheckout(atualizado);
  return atualizado;
}
