import { randomUUID } from "node:crypto";
import { getPool } from "../db/pool";
import { createPaidLedgerEntries } from "./ledgerStore";

export type TransactionStatus =
  | "initiated"
  | "awaiting_payment"
  | "paid"
  | "failed"
  | "expired"
  | "reversed";

export type TransactionEventType =
  | "INITIATED"
  | "STATE_CHANGED"
  | "CALLBACK_RECEIVED"
  | "CALLBACK_REJECTED"
  | "CALLBACK_DUPLICATE"
  | "PUSH_QUEUED"
  | "DYNAMIC_QR_CREATED";

export type TransactionEventSource = "webhook" | "system" | "admin" | "device";

export interface TransactionEntity {
  id: string;
  transaction_code: string;
  merchant_id: string;
  outlet_id: string;
  terminal_id: string;
  device_id?: string;
  qr_mode: "static" | "dynamic";
  initiation_mode: "static" | "manual" | "dynamic_api" | "dynamic_mqtt";
  partner_reference: string;
  amount: number;
  currency: string;
  status: TransactionStatus;
  created_at: string;
  paid_at?: string;
  expired_at?: string;
  updated_at: string;
}

export interface TransactionEventEntity {
  id: string;
  transaction_id: string;
  event_type: TransactionEventType;
  source: TransactionEventSource;
  payload_json: Record<string, unknown>;
  created_at: string;
}

function nowIso() {
  return new Date().toISOString();
}

function makeCode(id: string) {
  return `tx_${id.slice(0, 8)}`;
}

function mapTransaction(row: any): TransactionEntity {
  return {
    id: row.id,
    transaction_code: row.transaction_code,
    merchant_id: row.merchant_id,
    outlet_id: row.outlet_id,
    terminal_id: row.terminal_id,
    device_id: row.device_id || undefined,
    qr_mode: row.qr_mode,
    initiation_mode: row.initiation_mode,
    partner_reference: row.partner_reference,
    amount: Number(row.amount),
    currency: row.currency,
    status: row.status,
    created_at: row.created_at,
    paid_at: row.paid_at || undefined,
    expired_at: row.expired_at || undefined,
    updated_at: row.updated_at
  };
}

function mapEvent(row: any): TransactionEventEntity {
  return {
    id: row.id,
    transaction_id: row.transaction_id,
    event_type: row.event_type,
    source: row.source,
    payload_json: row.payload_json || {},
    created_at: row.created_at
  };
}

const TRANSACTION_STATE_TRANSITIONS: Record<TransactionStatus, TransactionStatus[]> = {
  initiated: ["initiated", "awaiting_payment", "paid", "failed", "expired", "reversed"],
  awaiting_payment: ["awaiting_payment", "paid", "failed", "expired", "reversed"],
  paid: ["paid", "reversed"],
  failed: ["failed", "reversed"],
  expired: ["expired", "reversed"],
  reversed: ["reversed"]
};

function isValidTransactionTransition(from: TransactionStatus, to: TransactionStatus) {
  return TRANSACTION_STATE_TRANSITIONS[from]?.includes(to) ?? false;
}

export async function createTransaction(payload: {
  merchant_id: string;
  outlet_id: string;
  terminal_id: string;
  device_id?: string;
  qr_mode?: "static" | "dynamic";
  initiation_mode?: "static" | "manual" | "dynamic_api" | "dynamic_mqtt";
  partner_reference: string;
  amount: number;
  currency?: string;
  status?: TransactionStatus;
  paid_at?: string;
  expired_at?: string;
}): Promise<TransactionEntity> {
  const id = randomUUID();
  const now = nowIso();

  const entity: TransactionEntity = {
    id,
    transaction_code: makeCode(id),
    merchant_id: payload.merchant_id,
    outlet_id: payload.outlet_id,
    terminal_id: payload.terminal_id,
    device_id: payload.device_id,
    qr_mode: payload.qr_mode || "static",
    initiation_mode: payload.initiation_mode || "static",
    partner_reference: payload.partner_reference,
    amount: payload.amount,
    currency: payload.currency || "IDR",
    status: payload.status || "initiated",
    created_at: now,
    paid_at: payload.paid_at,
    expired_at: payload.expired_at,
    updated_at: now
  };

  const txResult = await getPool().query(
    `INSERT INTO transactions (
      id,
      transaction_code,
      merchant_id,
      outlet_id,
      terminal_id,
      device_id,
      qr_mode,
      initiation_mode,
      partner_reference,
      amount,
      currency,
      status,
      created_at,
      paid_at,
      expired_at,
      updated_at
    ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16)
    RETURNING *`,
    [
      entity.id,
      entity.transaction_code,
      entity.merchant_id,
      entity.outlet_id,
      entity.terminal_id,
      entity.device_id || null,
      entity.qr_mode,
      entity.initiation_mode,
      entity.partner_reference,
      entity.amount,
      entity.currency,
      entity.status,
      entity.created_at,
      entity.paid_at || null,
      entity.expired_at || null,
      entity.updated_at
    ]
  );

  await addTransactionEvent({
    transaction_id: txResult.rows[0].id,
    event_type: "INITIATED",
    source: "system",
    payload_json: { status: txResult.rows[0].status, partner_reference: payload.partner_reference }
  });

  return mapTransaction(txResult.rows[0]);
}

export async function addTransactionEvent(payload: {
  transaction_id: string;
  event_type: TransactionEventType;
  source: TransactionEventSource;
  payload_json?: Record<string, unknown>;
}): Promise<TransactionEventEntity> {
  const id = randomUUID();
  const { rows } = await getPool().query(
    `INSERT INTO transaction_events (id, transaction_id, event_type, source, payload_json, created_at)
     VALUES ($1,$2,$3,$4,$5,$6)
     RETURNING *`,
    [id, payload.transaction_id, payload.event_type, payload.source, payload.payload_json || {}, nowIso()]
  );

  return mapEvent(rows[0]);
}

export async function updateTransactionStatus(
  id: string,
  to: TransactionStatus,
  options: {
    source: TransactionEventSource;
    eventContext?: Record<string, unknown>;
    paid_at?: string;
    expired_at?: string;
  }
): Promise<TransactionEntity> {
  const entity = await getTransactionById(id);
  if (!entity) {
    throw new Error("TRANSACTION_NOT_FOUND");
  }

  if (!isValidTransactionTransition(entity.status, to)) {
    throw new Error(`INVALID_TRANSACTION_STATE_TRANSITION:${entity.status}->${to}`);
  }

  if (entity.status === to) {
    return entity;
  }

  const now = nowIso();
  const next: TransactionEntity = {
    ...entity,
    status: to,
    paid_at: options.paid_at || entity.paid_at,
    expired_at: options.expired_at || entity.expired_at,
    updated_at: now
  };

  if (to === "paid" && !next.paid_at) {
    next.paid_at = now;
  }

  if (to === "expired" && !next.expired_at) {
    next.expired_at = now;
  }

  const { rows } = await getPool().query(
    `UPDATE transactions
        SET status = $2,
            paid_at = $3,
            expired_at = $4,
            updated_at = $5
      WHERE id = $1
      RETURNING *`,
    [id, next.status, next.paid_at || null, next.expired_at || null, next.updated_at]
  );

  await addTransactionEvent({
    transaction_id: id,
    event_type: "STATE_CHANGED",
    source: options.source,
    payload_json: {
      from: entity.status,
      to,
      ...options.eventContext
    }
  });

  const updated = mapTransaction(rows[0]);
  if (to === "paid") {
    await createPaidLedgerEntries(updated);
  }

  return updated;
}

export async function getTransactionById(id: string): Promise<TransactionEntity | null> {
  const { rows } = await getPool().query("SELECT * FROM transactions WHERE id = $1", [id]);
  return rows[0] ? mapTransaction(rows[0]) : null;
}

export async function findTransactionByPartnerReference(partnerReference: string): Promise<TransactionEntity | null> {
  const { rows } = await getPool().query(
    "SELECT * FROM transactions WHERE partner_reference = $1",
    [partnerReference]
  );
  return rows[0] ? mapTransaction(rows[0]) : null;
}

export async function listTransactions(filter?: {
  status?: TransactionStatus;
  merchant_id?: string;
}): Promise<TransactionEntity[]> {
  if (filter?.status && filter?.merchant_id) {
    const { rows } = await getPool().query(
      "SELECT * FROM transactions WHERE status = $1 AND merchant_id = $2 ORDER BY created_at DESC",
      [filter.status, filter.merchant_id]
    );
    return rows.map(mapTransaction);
  }

  if (filter?.status) {
    const { rows } = await getPool().query(
      "SELECT * FROM transactions WHERE status = $1 ORDER BY created_at DESC",
      [filter.status]
    );
    return rows.map(mapTransaction);
  }

  if (filter?.merchant_id) {
    const { rows } = await getPool().query(
      "SELECT * FROM transactions WHERE merchant_id = $1 ORDER BY created_at DESC",
      [filter.merchant_id]
    );
    return rows.map(mapTransaction);
  }

  const { rows } = await getPool().query("SELECT * FROM transactions ORDER BY created_at DESC");
  return rows.map(mapTransaction);
}

export async function listDueDynamicQrTransactions(limit = 100): Promise<TransactionEntity[]> {
  const safeLimit = Math.min(Math.max(limit, 1), 500);
  const { rows } = await getPool().query(
    `SELECT * FROM transactions
      WHERE qr_mode = 'dynamic'
        AND status = 'awaiting_payment'
        AND expired_at IS NOT NULL
        AND expired_at <= NOW()
      ORDER BY expired_at ASC
      LIMIT ${safeLimit}`
  );

  return rows.map(mapTransaction);
}

export async function getTransactionEvents(transactionId: string): Promise<TransactionEventEntity[]> {
  const { rows } = await getPool().query(
    "SELECT * FROM transaction_events WHERE transaction_id = $1 ORDER BY created_at ASC",
    [transactionId]
  );
  return rows.map(mapEvent);
}

export function toTransactionPayload(transaction: TransactionEntity) {
  return { ...transaction };
}

export function toTransactionEventPayload(event: TransactionEventEntity) {
  return { ...event, payload_json: { ...event.payload_json } };
}
