import { randomUUID } from "node:crypto";
import { getPool, withClient } from "../db/pool";
import { env } from "../../config/env";

export type SettlementBatchStatus = "created" | "paid" | "failed" | "cancelled";

export interface SettlementBatchEntity {
  id: string;
  batch_code: string;
  merchant_id: string;
  currency: string;
  gross_amount: number;
  platform_fee_amount: number;
  net_payable_amount: number;
  entry_count: number;
  status: SettlementBatchStatus;
  cutoff_at: string;
  created_at: string;
  paid_at?: string;
  failure_reason?: string;
  metadata_json: Record<string, unknown>;
}

export interface SettlementBatchEntryEntity {
  id: string;
  batch_id: string;
  ledger_entry_id: string;
  transaction_id: string;
  merchant_id: string;
  amount: number;
  currency: string;
  created_at: string;
}

export interface SettlementBatchAdjustmentEntity {
  id: string;
  batch_id: string;
  merchant_id: string;
  adjustment_type: "credit" | "debit";
  amount: number;
  signed_amount: number;
  currency: string;
  reason: string;
  note?: string;
  approval_status: "pending" | "approved" | "rejected";
  approved_by?: string;
  approved_at?: string;
  rejected_by?: string;
  rejected_at?: string;
  actor_type: "admin" | "merchant" | "system";
  actor_id?: string;
  metadata_json: Record<string, unknown>;
  created_at: string;
}

export interface SettlementBatchReportRow {
  batch_code: string;
  batch_status: SettlementBatchStatus;
  merchant_id: string;
  merchant_name?: string;
  settlement_account_reference?: string;
  settlement_account_type?: string;
  transaction_id: string;
  transaction_code: string;
  partner_reference: string;
  paid_at?: string;
  gross_amount: number;
  platform_fee_amount: number;
  net_payable_amount: number;
  currency: string;
}

export interface SettlementFinanceSummary {
  pending_amount: number;
  paid_amount: number;
  adjustment_amount: number;
  adjusted_paid_amount: number;
  platform_fee_amount: number;
  created_batches: number;
  paid_batches: number;
  total_batches: number;
}

export interface SettlementReconciliationIssue {
  type:
    | "entry_count_mismatch"
    | "gross_amount_mismatch"
    | "platform_fee_mismatch"
    | "net_payable_mismatch"
    | "missing_transaction"
    | "missing_gross_ledger"
    | "missing_fee_ledger"
    | "missing_payable_ledger"
    | "transaction_status_mismatch";
  severity: "warning" | "critical";
  message: string;
  expected?: number | string;
  actual?: number | string;
}

export interface SettlementReconciliationRow {
  batch: SettlementBatchEntity;
  computed: {
    entry_count: number;
    gross_amount: number;
    platform_fee_amount: number;
    net_payable_amount: number;
    paid_transaction_count: number;
    missing_transaction_count: number;
    missing_gross_count: number;
    missing_fee_count: number;
    missing_payable_count: number;
    archived_reprocessed: boolean;
  };
  issue_count: number;
  status: "matched" | "mismatch";
  issues: SettlementReconciliationIssue[];
}

export interface SettlementReconciliationReport {
  generated_at: string;
  total_batches: number;
  matched_batches: number;
  mismatch_batches: number;
  issue_count: number;
  rows: SettlementReconciliationRow[];
}

export interface SettlementAdjustmentReportRow extends SettlementBatchAdjustmentEntity {
  batch_code: string;
  batch_status: SettlementBatchStatus;
  batch_created_at: string;
  batch_paid_at?: string;
  net_payable_amount: number;
}

export interface SettlementAdjustmentReport {
  generated_at: string;
  total_count: number;
  credit_amount: number;
  debit_amount: number;
  signed_amount: number;
  rows: SettlementAdjustmentReportRow[];
}

export interface SettlementBatchEventEntity {
  id: string;
  batch_id: string;
  merchant_id: string;
  event_type:
    | "created"
    | "csv_exported"
    | "marked_paid"
    | "failed"
    | "cancelled"
    | "reprocessed"
    | "reference_updated"
    | "adjustment_recorded"
    | "adjustment_approved"
    | "adjustment_rejected";
  actor_type: "admin" | "merchant" | "system";
  actor_id?: string;
  payload_json: Record<string, unknown>;
  created_at: string;
}

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

function makeBatchCode(id: string) {
  const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, "");
  return `SET-${stamp}-${id.slice(0, 8)}`;
}

function roundMoney(value: number) {
  return Math.round((value + Number.EPSILON) * 100) / 100;
}

function moneyDiffers(expected: number, actual: number) {
  return Math.abs(roundMoney(expected) - roundMoney(actual)) > 0.01;
}

function mapBatch(row: any): SettlementBatchEntity {
  return {
    id: row.id,
    batch_code: row.batch_code,
    merchant_id: row.merchant_id,
    currency: row.currency,
    gross_amount: Number(row.gross_amount),
    platform_fee_amount: Number(row.platform_fee_amount),
    net_payable_amount: Number(row.net_payable_amount),
    entry_count: Number(row.entry_count),
    status: row.status,
    cutoff_at: row.cutoff_at,
    created_at: row.created_at,
    paid_at: row.paid_at || undefined,
    failure_reason: row.failure_reason || undefined,
    metadata_json: row.metadata_json || {}
  };
}

function mapBatchEntry(row: any): SettlementBatchEntryEntity {
  return {
    id: row.id,
    batch_id: row.batch_id,
    ledger_entry_id: row.ledger_entry_id,
    transaction_id: row.transaction_id,
    merchant_id: row.merchant_id,
    amount: Number(row.amount),
    currency: row.currency,
    created_at: row.created_at
  };
}

function mapBatchAdjustment(row: any): SettlementBatchAdjustmentEntity {
  return {
    id: row.id,
    batch_id: row.batch_id,
    merchant_id: row.merchant_id,
    adjustment_type: row.adjustment_type,
    amount: Number(row.amount),
    signed_amount: Number(row.signed_amount),
    currency: row.currency,
    reason: row.reason,
    note: row.note || undefined,
    approval_status: row.approval_status || "approved",
    approved_by: row.approved_by || undefined,
    approved_at: row.approved_at || undefined,
    rejected_by: row.rejected_by || undefined,
    rejected_at: row.rejected_at || undefined,
    actor_type: row.actor_type,
    actor_id: row.actor_id || undefined,
    metadata_json: row.metadata_json || {},
    created_at: row.created_at
  };
}

function mapAdjustmentReportRow(row: any): SettlementAdjustmentReportRow {
  return {
    ...mapBatchAdjustment(row),
    batch_code: row.batch_code,
    batch_status: row.batch_status,
    batch_created_at: row.batch_created_at,
    batch_paid_at: row.batch_paid_at || undefined,
    net_payable_amount: Number(row.net_payable_amount || 0)
  };
}

function mapBatchEvent(row: any): SettlementBatchEventEntity {
  return {
    id: row.id,
    batch_id: row.batch_id,
    merchant_id: row.merchant_id,
    event_type: row.event_type,
    actor_type: row.actor_type,
    actor_id: row.actor_id || undefined,
    payload_json: row.payload_json || {},
    created_at: row.created_at
  };
}

export async function createSettlementBatches(input?: {
  merchant_id?: string;
  cutoff_at?: string;
  limit_per_merchant?: number;
}): Promise<SettlementBatchEntity[]> {
  const cutoffAt = input?.cutoff_at || nowIso();
  const limitPerMerchant = Math.min(Math.max(input?.limit_per_merchant || 500, 1), 1000);

  return withClient(async (client) => {
    await client.query("BEGIN");
    try {
      const merchantParams: unknown[] = [cutoffAt];
      const merchantFilter = input?.merchant_id ? "AND le.merchant_id = $2" : "";
      if (input?.merchant_id) {
        merchantParams.push(input.merchant_id);
      }

      const { rows: merchants } = await client.query(
        `SELECT le.merchant_id
           FROM ledger_entries le
           LEFT JOIN settlement_batch_entries sbe ON sbe.ledger_entry_id = le.id
          WHERE le.entry_type = 'merchant_payable'
            AND le.status = 'posted'
            AND le.created_at <= $1
            AND sbe.id IS NULL
            ${merchantFilter}
          GROUP BY le.merchant_id
          ORDER BY le.merchant_id ASC`,
        merchantParams
      );

      const batches: SettlementBatchEntity[] = [];
      for (const row of merchants) {
        const merchantId = row.merchant_id as string;
        const { rows: entries } = await client.query(
          `SELECT le.*
             FROM ledger_entries le
             LEFT JOIN settlement_batch_entries sbe ON sbe.ledger_entry_id = le.id
            WHERE le.entry_type = 'merchant_payable'
              AND le.status = 'posted'
              AND le.merchant_id = $1
              AND le.created_at <= $2
              AND sbe.id IS NULL
            ORDER BY le.created_at ASC
            LIMIT ${limitPerMerchant}`,
          [merchantId, cutoffAt]
        );

        if (!entries.length) {
          continue;
        }

        const transactionIds = entries.map((entry) => entry.transaction_id);
        const { rows: totals } = await client.query(
          `SELECT
              COALESCE(SUM(CASE WHEN entry_type = 'gross_income' THEN amount ELSE 0 END), 0) AS gross_amount,
              COALESCE(SUM(CASE WHEN entry_type = 'platform_fee' THEN amount ELSE 0 END), 0) AS platform_fee_amount
             FROM ledger_entries
            WHERE merchant_id = $1
              AND transaction_id = ANY($2::text[])
              AND status = 'posted'`,
          [merchantId, transactionIds]
        );

        const id = randomUUID();
        const now = nowIso();
        const netPayable = entries.reduce((sum, entry) => sum + Number(entry.amount), 0);
        const currency = entries[0].currency || "IDR";
        const { rows: batchRows } = await client.query(
          `INSERT INTO settlement_batches (
            id,
            batch_code,
            merchant_id,
            currency,
            gross_amount,
            platform_fee_amount,
            net_payable_amount,
            entry_count,
            status,
            cutoff_at,
            created_at,
            metadata_json
          ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)
          RETURNING *`,
          [
            id,
            makeBatchCode(id),
            merchantId,
            currency,
            Number(totals[0]?.gross_amount || 0),
            Number(totals[0]?.platform_fee_amount || 0),
            netPayable,
            entries.length,
            "created",
            cutoffAt,
            now,
            {
              source: "settlement_batch_light",
              limit_per_merchant: limitPerMerchant
            }
          ]
        );

        for (const entry of entries) {
          await client.query(
            `INSERT INTO settlement_batch_entries (
              id,
              batch_id,
              ledger_entry_id,
              transaction_id,
              merchant_id,
              amount,
              currency,
              created_at
            ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8)
            ON CONFLICT (ledger_entry_id) DO NOTHING`,
            [
              randomUUID(),
              id,
              entry.id,
              entry.transaction_id,
              entry.merchant_id,
              Number(entry.amount),
              entry.currency,
              now
            ]
          );
        }

        batches.push(mapBatch(batchRows[0]));
      }

      await client.query("COMMIT");
      return batches;
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    }
  });
}

export async function listSettlementBatches(filter?: {
  merchant_id?: string;
  status?: SettlementBatchStatus;
  limit?: number;
}): Promise<SettlementBatchEntity[]> {
  const clauses: string[] = [];
  const params: unknown[] = [];
  let i = 1;

  if (filter?.merchant_id) {
    clauses.push(`merchant_id = $${i++}`);
    params.push(filter.merchant_id);
  }

  if (filter?.status) {
    clauses.push(`status = $${i++}`);
    params.push(filter.status);
  }

  const limit = Math.min(Math.max(filter?.limit || 100, 1), 500);
  const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
  const { rows } = await getPool().query(
    `SELECT * FROM settlement_batches ${where} ORDER BY created_at DESC LIMIT ${limit}`,
    params
  );

  return rows.map(mapBatch);
}

export async function getSettlementFinanceSummary(filter?: { merchant_id?: string }): Promise<SettlementFinanceSummary> {
  const params: unknown[] = [];
  const where = filter?.merchant_id ? "WHERE sb.merchant_id = $1" : "";
  if (filter?.merchant_id) {
    params.push(filter.merchant_id);
  }

  const { rows } = await getPool().query(
    `WITH adjustment_totals AS (
       SELECT batch_id, COALESCE(SUM(signed_amount), 0) AS adjustment_amount
         FROM settlement_batch_adjustments
        WHERE approval_status = 'approved'
        GROUP BY batch_id
     )
     SELECT
        COALESCE(SUM(CASE WHEN sb.status = 'created' THEN sb.net_payable_amount ELSE 0 END), 0) AS pending_amount,
        COALESCE(SUM(CASE WHEN sb.status = 'paid' THEN sb.net_payable_amount ELSE 0 END), 0) AS paid_amount,
        COALESCE(SUM(COALESCE(at.adjustment_amount, 0)), 0) AS adjustment_amount,
        COALESCE(SUM(sb.platform_fee_amount), 0) AS platform_fee_amount,
        COUNT(*) FILTER (WHERE sb.status = 'created') AS created_batches,
        COUNT(*) FILTER (WHERE sb.status = 'paid') AS paid_batches,
        COUNT(*) AS total_batches
       FROM settlement_batches sb
       LEFT JOIN adjustment_totals at ON at.batch_id = sb.id
       ${where}`,
    params
  );
  const row = rows[0] || {};
  return {
    pending_amount: Number(row.pending_amount || 0),
    paid_amount: Number(row.paid_amount || 0),
    adjustment_amount: Number(row.adjustment_amount || 0),
    adjusted_paid_amount: Number(row.paid_amount || 0) + Number(row.adjustment_amount || 0),
    platform_fee_amount: Number(row.platform_fee_amount || 0),
    created_batches: Number(row.created_batches || 0),
    paid_batches: Number(row.paid_batches || 0),
    total_batches: Number(row.total_batches || 0)
  };
}

export async function getSettlementReconciliationReport(filter?: {
  merchant_id?: string;
  status?: SettlementBatchStatus;
  limit?: number;
  mismatches_only?: boolean;
}): Promise<SettlementReconciliationReport> {
  const batches = await listSettlementBatches({
    merchant_id: filter?.merchant_id,
    status: filter?.status,
    limit: filter?.limit || 100
  });

  const rows: SettlementReconciliationRow[] = [];

  for (const batch of batches) {
    const { rows: entryRows } = await getPool().query(
      `SELECT
          sbe.id,
          sbe.amount AS settlement_amount,
          sbe.transaction_id,
          tx.id AS tx_id,
          tx.status AS tx_status,
          gross.id AS gross_ledger_id,
          COALESCE(gross.amount, 0) AS gross_amount,
          fee.id AS fee_ledger_id,
          COALESCE(fee.amount, 0) AS platform_fee_amount,
          payable.id AS payable_ledger_id,
          COALESCE(payable.amount, sbe.amount, 0) AS merchant_payable_amount
        FROM settlement_batch_entries sbe
        LEFT JOIN transactions tx ON tx.id = sbe.transaction_id
        LEFT JOIN ledger_entries gross
          ON gross.transaction_id = sbe.transaction_id
         AND gross.entry_type = 'gross_income'
         AND gross.status = 'posted'
        LEFT JOIN ledger_entries fee
          ON fee.transaction_id = sbe.transaction_id
         AND fee.entry_type = 'platform_fee'
         AND fee.status = 'posted'
        LEFT JOIN ledger_entries payable
          ON payable.id = sbe.ledger_entry_id
         AND payable.entry_type = 'merchant_payable'
         AND payable.status = 'posted'
       WHERE sbe.batch_id = $1
       ORDER BY sbe.created_at ASC`,
      [batch.id]
    );

    const computed = entryRows.reduce(
      (acc, row) => {
        acc.entry_count += 1;
        acc.gross_amount += Number(row.gross_amount || 0);
        acc.platform_fee_amount += Number(row.platform_fee_amount || 0);
        acc.net_payable_amount += Number(row.merchant_payable_amount || row.settlement_amount || 0);
        if (!row.tx_id) {
          acc.missing_transaction_count += 1;
        }
        if (row.tx_status === "paid") {
          acc.paid_transaction_count += 1;
        }
        if (!row.gross_ledger_id) {
          acc.missing_gross_count += 1;
        }
        if (!row.fee_ledger_id) {
          acc.missing_fee_count += 1;
        }
        if (!row.payable_ledger_id) {
          acc.missing_payable_count += 1;
        }
        return acc;
      },
      {
        entry_count: 0,
        gross_amount: 0,
        platform_fee_amount: 0,
        net_payable_amount: 0,
        paid_transaction_count: 0,
        missing_transaction_count: 0,
        missing_gross_count: 0,
        missing_fee_count: 0,
        missing_payable_count: 0,
        archived_reprocessed: Boolean(batch.metadata_json?.reprocessed_to_batch_id)
      }
    );

    computed.gross_amount = roundMoney(computed.gross_amount);
    computed.platform_fee_amount = roundMoney(computed.platform_fee_amount);
    computed.net_payable_amount = roundMoney(computed.net_payable_amount);

    const issues: SettlementReconciliationIssue[] = [];
    const shouldCompareAmounts = !computed.archived_reprocessed;

    if (shouldCompareAmounts && computed.entry_count !== batch.entry_count) {
      issues.push({
        type: "entry_count_mismatch",
        severity: "critical",
        message: "Settlement entry count does not match batch aggregate",
        expected: batch.entry_count,
        actual: computed.entry_count
      });
    }
    if (shouldCompareAmounts && moneyDiffers(batch.gross_amount, computed.gross_amount)) {
      issues.push({
        type: "gross_amount_mismatch",
        severity: "critical",
        message: "Gross amount does not match posted gross ledger entries",
        expected: batch.gross_amount,
        actual: computed.gross_amount
      });
    }
    if (shouldCompareAmounts && moneyDiffers(batch.platform_fee_amount, computed.platform_fee_amount)) {
      issues.push({
        type: "platform_fee_mismatch",
        severity: "critical",
        message: "Platform fee amount does not match posted fee ledger entries",
        expected: batch.platform_fee_amount,
        actual: computed.platform_fee_amount
      });
    }
    if (shouldCompareAmounts && moneyDiffers(batch.net_payable_amount, computed.net_payable_amount)) {
      issues.push({
        type: "net_payable_mismatch",
        severity: "critical",
        message: "Net payable amount does not match merchant payable ledger entries",
        expected: batch.net_payable_amount,
        actual: computed.net_payable_amount
      });
    }
    if (computed.missing_transaction_count > 0) {
      issues.push({
        type: "missing_transaction",
        severity: "critical",
        message: "One or more settlement entries no longer have a transaction record",
        expected: 0,
        actual: computed.missing_transaction_count
      });
    }
    if (computed.missing_gross_count > 0) {
      issues.push({
        type: "missing_gross_ledger",
        severity: "critical",
        message: "One or more settlement entries are missing gross ledger entries",
        expected: 0,
        actual: computed.missing_gross_count
      });
    }
    if (computed.missing_fee_count > 0) {
      issues.push({
        type: "missing_fee_ledger",
        severity: "critical",
        message: "One or more settlement entries are missing platform fee ledger entries",
        expected: 0,
        actual: computed.missing_fee_count
      });
    }
    if (computed.missing_payable_count > 0) {
      issues.push({
        type: "missing_payable_ledger",
        severity: "critical",
        message: "One or more settlement entries are missing merchant payable ledger entries",
        expected: 0,
        actual: computed.missing_payable_count
      });
    }

    const nonPaidCount = entryRows.filter((row) => row.tx_id && row.tx_status !== "paid").length;
    if (nonPaidCount > 0) {
      issues.push({
        type: "transaction_status_mismatch",
        severity: "critical",
        message: "One or more settlement entries are linked to non-paid transactions",
        expected: "paid",
        actual: `${nonPaidCount} non-paid transaction(s)`
      });
    }

    rows.push({
      batch,
      computed,
      issue_count: issues.length,
      status: issues.length > 0 ? "mismatch" : "matched",
      issues
    });
  }

  const visibleRows = filter?.mismatches_only ? rows.filter((row) => row.status === "mismatch") : rows;
  const mismatchBatches = rows.filter((row) => row.status === "mismatch").length;
  const issueCount = rows.reduce((sum, row) => sum + row.issue_count, 0);

  return {
    generated_at: nowIso(),
    total_batches: rows.length,
    matched_batches: rows.length - mismatchBatches,
    mismatch_batches: mismatchBatches,
    issue_count: issueCount,
    rows: visibleRows
  };
}

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

export async function getSettlementBatchByIdForMerchant(
  id: string,
  merchantId: string
): Promise<SettlementBatchEntity | null> {
  const { rows } = await getPool().query("SELECT * FROM settlement_batches WHERE id = $1 AND merchant_id = $2", [
    id,
    merchantId
  ]);
  return rows[0] ? mapBatch(rows[0]) : null;
}

export async function listSettlementBatchEntries(batchId: string): Promise<SettlementBatchEntryEntity[]> {
  const { rows } = await getPool().query(
    "SELECT * FROM settlement_batch_entries WHERE batch_id = $1 ORDER BY created_at ASC",
    [batchId]
  );
  return rows.map(mapBatchEntry);
}

export async function listSettlementBatchAdjustments(batchId: string): Promise<SettlementBatchAdjustmentEntity[]> {
  const { rows } = await getPool().query(
    "SELECT * FROM settlement_batch_adjustments WHERE batch_id = $1 ORDER BY created_at ASC",
    [batchId]
  );
  return rows.map(mapBatchAdjustment);
}

export async function listSettlementAdjustments(filter?: {
  merchant_id?: string;
  adjustment_type?: "credit" | "debit";
  approval_status?: "pending" | "approved" | "rejected";
  from?: string;
  to?: string;
  limit?: number;
  max_limit?: number;
}): Promise<SettlementAdjustmentReport> {
  const clauses: string[] = [];
  const params: unknown[] = [];
  let i = 1;

  if (filter?.merchant_id) {
    clauses.push(`sba.merchant_id = $${i++}`);
    params.push(filter.merchant_id);
  }
  if (filter?.adjustment_type) {
    clauses.push(`sba.adjustment_type = $${i++}`);
    params.push(filter.adjustment_type);
  }
  if (filter?.approval_status) {
    clauses.push(`sba.approval_status = $${i++}`);
    params.push(filter.approval_status);
  }
  if (filter?.from) {
    clauses.push(`sba.created_at >= $${i++}`);
    params.push(filter.from);
  }
  if (filter?.to) {
    clauses.push(`sba.created_at <= $${i++}`);
    params.push(filter.to);
  }

  const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
  const maxLimit = Math.min(Math.max(filter?.max_limit || 500, 1), 10000);
  const limit = Math.min(Math.max(filter?.limit || 100, 1), maxLimit);
  const summaryParams = [...params];
  const rowParams = [...params, limit];

  const [summaryResult, rowResult] = await Promise.all([
    getPool().query(
      `SELECT
          COUNT(*) AS total_count,
          COALESCE(SUM(CASE WHEN sba.adjustment_type = 'credit' THEN sba.amount ELSE 0 END), 0) AS credit_amount,
          COALESCE(SUM(CASE WHEN sba.adjustment_type = 'debit' THEN sba.amount ELSE 0 END), 0) AS debit_amount,
          COALESCE(SUM(sba.signed_amount), 0) AS signed_amount
         FROM settlement_batch_adjustments sba
         JOIN settlement_batches sb ON sb.id = sba.batch_id
         ${where}`,
      summaryParams
    ),
    getPool().query(
      `SELECT
          sba.*,
          sb.batch_code,
          sb.status AS batch_status,
          sb.created_at AS batch_created_at,
          sb.paid_at AS batch_paid_at,
          sb.net_payable_amount
         FROM settlement_batch_adjustments sba
         JOIN settlement_batches sb ON sb.id = sba.batch_id
         ${where}
         ORDER BY sba.created_at DESC
         LIMIT $${i}`,
      rowParams
    )
  ]);

  const summary = summaryResult.rows[0] || {};
  return {
    generated_at: nowIso(),
    total_count: Number(summary.total_count || 0),
    credit_amount: Number(summary.credit_amount || 0),
    debit_amount: Number(summary.debit_amount || 0),
    signed_amount: Number(summary.signed_amount || 0),
    rows: rowResult.rows.map(mapAdjustmentReportRow)
  };
}

export async function createSettlementBatchEvent(payload: {
  batch_id: string;
  merchant_id: string;
  event_type: SettlementBatchEventEntity["event_type"];
  actor_type: SettlementBatchEventEntity["actor_type"];
  actor_id?: string;
  payload_json?: Record<string, unknown>;
}): Promise<SettlementBatchEventEntity> {
  const { rows } = await getPool().query(
    `INSERT INTO settlement_batch_events (
      id,
      batch_id,
      merchant_id,
      event_type,
      actor_type,
      actor_id,
      payload_json,
      created_at
    ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8)
    RETURNING *`,
    [
      randomUUID(),
      payload.batch_id,
      payload.merchant_id,
      payload.event_type,
      payload.actor_type,
      payload.actor_id || null,
      payload.payload_json || {},
      nowIso()
    ]
  );

  return mapBatchEvent(rows[0]);
}

export async function listSettlementBatchEvents(batchId: string): Promise<SettlementBatchEventEntity[]> {
  const { rows } = await getPool().query(
    "SELECT * FROM settlement_batch_events WHERE batch_id = $1 ORDER BY created_at ASC",
    [batchId]
  );
  return rows.map(mapBatchEvent);
}

export async function getSettlementBatchReportRows(batchId: string): Promise<SettlementBatchReportRow[]> {
  const { rows } = await getPool().query(
    `SELECT
        sb.batch_code,
        sb.status AS batch_status,
        sb.merchant_id,
        COALESCE(m.brand_name, m.legal_name) AS merchant_name,
        m.settlement_account_reference,
        m.settlement_account_type,
        sbe.transaction_id,
        tx.transaction_code,
        tx.partner_reference,
        tx.paid_at,
        COALESCE(gross.amount, 0) AS gross_amount,
        COALESCE(fee.amount, 0) AS platform_fee_amount,
        sbe.amount AS net_payable_amount,
        sbe.currency
      FROM settlement_batch_entries sbe
      JOIN settlement_batches sb ON sb.id = sbe.batch_id
      JOIN merchants m ON m.id = sbe.merchant_id
      JOIN transactions tx ON tx.id = sbe.transaction_id
      LEFT JOIN ledger_entries gross
        ON gross.transaction_id = sbe.transaction_id
       AND gross.entry_type = 'gross_income'
      LEFT JOIN ledger_entries fee
        ON fee.transaction_id = sbe.transaction_id
       AND fee.entry_type = 'platform_fee'
      WHERE sbe.batch_id = $1
      ORDER BY tx.paid_at ASC NULLS LAST, tx.created_at ASC`,
    [batchId]
  );

  return rows.map((row) => ({
    batch_code: row.batch_code,
    batch_status: row.batch_status,
    merchant_id: row.merchant_id,
    merchant_name: row.merchant_name || undefined,
    settlement_account_reference: row.settlement_account_reference || undefined,
    settlement_account_type: row.settlement_account_type || undefined,
    transaction_id: row.transaction_id,
    transaction_code: row.transaction_code,
    partner_reference: row.partner_reference,
    paid_at: row.paid_at || undefined,
    gross_amount: Number(row.gross_amount),
    platform_fee_amount: Number(row.platform_fee_amount),
    net_payable_amount: Number(row.net_payable_amount),
    currency: row.currency
  }));
}

export async function markSettlementBatchPaid(
  id: string,
  input?: {
    paid_at?: string;
    paid_reference?: string;
    paid_note?: string;
  }
): Promise<SettlementBatchEntity> {
  const existing = await getSettlementBatchById(id);
  if (!existing) {
    throw new Error("SETTLEMENT_BATCH_NOT_FOUND");
  }
  if (existing.status !== "created") {
    throw new Error("SETTLEMENT_BATCH_NOT_PAYABLE");
  }

  const { rows } = await getPool().query(
    `UPDATE settlement_batches
        SET status = 'paid',
            paid_at = $2,
            failure_reason = NULL,
            metadata_json = metadata_json || $3::jsonb
      WHERE id = $1
      RETURNING *`,
    [
      id,
      input?.paid_at || nowIso(),
      {
        paid_source: "admin_mark_paid",
        paid_reference: input?.paid_reference || null,
        paid_note: input?.paid_note || null,
        paid_marked_at: nowIso()
      }
    ]
  );

  return mapBatch(rows[0]);
}

export async function updateSettlementBatchReference(
  id: string,
  input: {
    paid_reference: string;
    paid_note?: string;
  }
): Promise<SettlementBatchEntity> {
  const existing = await getSettlementBatchById(id);
  if (!existing) {
    throw new Error("SETTLEMENT_BATCH_NOT_FOUND");
  }
  if (existing.status !== "paid") {
    throw new Error("SETTLEMENT_BATCH_REFERENCE_NOT_EDITABLE");
  }

  const { rows } = await getPool().query(
    `UPDATE settlement_batches
        SET metadata_json = metadata_json || $2::jsonb
      WHERE id = $1
      RETURNING *`,
    [
      id,
      {
        paid_reference: input.paid_reference,
        paid_note: input.paid_note || null,
        reference_update_source: "admin",
        reference_updated_at: nowIso()
      }
    ]
  );

  return mapBatch(rows[0]);
}

export async function recordSettlementBatchAdjustment(
  id: string,
  input: {
    adjustment_type: "credit" | "debit";
    amount: number;
    reason: string;
    note?: string;
    actor_id?: string;
  }
): Promise<SettlementBatchEntity> {
  return withClient(async (client) => {
    await client.query("BEGIN");
    try {
      const { rows: batchRows } = await client.query("SELECT * FROM settlement_batches WHERE id = $1 FOR UPDATE", [id]);
      if (!batchRows.length) {
        throw new Error("SETTLEMENT_BATCH_NOT_FOUND");
      }

      const existing = mapBatch(batchRows[0]);
      if (existing.metadata_json?.reprocessed_to_batch_id) {
        throw new Error("SETTLEMENT_BATCH_ADJUSTMENT_NOT_EDITABLE");
      }

      const adjustmentAmount = roundMoney(Math.abs(input.amount));
      const signedAmount = input.adjustment_type === "credit" ? adjustmentAmount : -adjustmentAmount;
      const adjustmentId = randomUUID();
      const createdAt = nowIso();
      const approvalRequired = String(env.SETTLEMENT_ADJUSTMENT_REQUIRE_APPROVAL).toLowerCase() === "true";
      const approvalStatus = approvalRequired ? "pending" : "approved";
      await client.query(
        `INSERT INTO settlement_batch_adjustments (
          id,
          batch_id,
          merchant_id,
          adjustment_type,
          amount,
          signed_amount,
          currency,
          reason,
          note,
          approval_status,
          approved_by,
          approved_at,
          actor_type,
          actor_id,
          metadata_json,
          created_at
        ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16)`,
        [
          adjustmentId,
          existing.id,
          existing.merchant_id,
          input.adjustment_type,
          adjustmentAmount,
          signedAmount,
          existing.currency,
          input.reason,
          input.note || null,
          approvalStatus,
          approvalRequired ? null : input.actor_id || "admin",
          approvalRequired ? null : createdAt,
          "admin",
          input.actor_id || null,
          { source: "settlement_adjustment_formal", approval_required: approvalRequired },
          createdAt
        ]
      );

      const { rows: adjustmentRows } = await client.query(
        "SELECT * FROM settlement_batch_adjustments WHERE batch_id = $1 ORDER BY created_at ASC",
        [existing.id]
      );
      const adjustments = adjustmentRows.map(mapBatchAdjustment);
      const totalAdjustmentAmount = adjustments
        .filter((item) => item.approval_status === "approved")
        .reduce((sum, item) => sum + Number(item.signed_amount || 0), 0);
      const { rows: updatedRows } = await client.query(
        `UPDATE settlement_batches
            SET metadata_json = metadata_json || $2::jsonb
          WHERE id = $1
          RETURNING *`,
        [
          existing.id,
          {
            adjustments,
            total_adjustment_amount: roundMoney(totalAdjustmentAmount),
            last_adjustment_id: adjustmentId,
            last_adjustment_at: createdAt,
            last_adjustment_approval_status: approvalStatus,
            adjustment_source: "settlement_batch_adjustments"
          }
        ]
      );

      await client.query("COMMIT");
      return mapBatch(updatedRows[0]);
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    }
  });
}

export async function transitionSettlementBatchAdjustmentApproval(
  adjustmentId: string,
  input: {
    approval_status: "approved" | "rejected";
    actor_id?: string;
    note?: string;
  }
): Promise<{ adjustment: SettlementBatchAdjustmentEntity; batch: SettlementBatchEntity }> {
  return withClient(async (client) => {
    await client.query("BEGIN");
    try {
      const { rows: adjustmentRows } = await client.query(
        "SELECT * FROM settlement_batch_adjustments WHERE id = $1 FOR UPDATE",
        [adjustmentId]
      );
      if (!adjustmentRows.length) {
        throw new Error("SETTLEMENT_ADJUSTMENT_NOT_FOUND");
      }

      const existing = mapBatchAdjustment(adjustmentRows[0]);
      if (existing.approval_status !== "pending") {
        throw new Error("SETTLEMENT_ADJUSTMENT_NOT_PENDING");
      }

      const now = nowIso();
      const metadataPatch =
        input.approval_status === "approved"
          ? {
              approval_note: input.note || null,
              approved_source: "admin"
            }
          : {
              rejection_note: input.note || null,
              rejected_source: "admin"
            };
      const { rows: updatedAdjustmentRows } = await client.query(
        `UPDATE settlement_batch_adjustments
            SET approval_status = $2,
                approved_by = CASE WHEN $2 = 'approved' THEN $3 ELSE approved_by END,
                approved_at = CASE WHEN $2 = 'approved' THEN $4 ELSE approved_at END,
                rejected_by = CASE WHEN $2 = 'rejected' THEN $3 ELSE rejected_by END,
                rejected_at = CASE WHEN $2 = 'rejected' THEN $4 ELSE rejected_at END,
                metadata_json = metadata_json || $5::jsonb
          WHERE id = $1
          RETURNING *`,
        [adjustmentId, input.approval_status, input.actor_id || "admin", now, metadataPatch]
      );

      const { rows: allAdjustmentRows } = await client.query(
        "SELECT * FROM settlement_batch_adjustments WHERE batch_id = $1 ORDER BY created_at ASC",
        [existing.batch_id]
      );
      const adjustments = allAdjustmentRows.map(mapBatchAdjustment);
      const totalAdjustmentAmount = adjustments
        .filter((item) => item.approval_status === "approved")
        .reduce((sum, item) => sum + Number(item.signed_amount || 0), 0);
      const { rows: batchRows } = await client.query(
        `UPDATE settlement_batches
            SET metadata_json = metadata_json || $2::jsonb
          WHERE id = $1
          RETURNING *`,
        [
          existing.batch_id,
          {
            adjustments,
            total_adjustment_amount: roundMoney(totalAdjustmentAmount),
            last_adjustment_id: adjustmentId,
            last_adjustment_approval_status: input.approval_status,
            last_adjustment_approval_at: now,
            adjustment_source: "settlement_batch_adjustments"
          }
        ]
      );

      await client.query("COMMIT");
      return {
        adjustment: mapBatchAdjustment(updatedAdjustmentRows[0]),
        batch: mapBatch(batchRows[0])
      };
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    }
  });
}

export async function transitionSettlementBatchStatus(
  id: string,
  input: {
    status: Extract<SettlementBatchStatus, "failed" | "cancelled">;
    reason: string;
    note?: string;
  }
): Promise<SettlementBatchEntity> {
  const existing = await getSettlementBatchById(id);
  if (!existing) {
    throw new Error("SETTLEMENT_BATCH_NOT_FOUND");
  }
  if (existing.status !== "created") {
    throw new Error("SETTLEMENT_BATCH_NOT_TRANSITIONABLE");
  }

  const { rows } = await getPool().query(
    `UPDATE settlement_batches
        SET status = $2,
            paid_at = NULL,
            failure_reason = $3,
            metadata_json = metadata_json || $4::jsonb
      WHERE id = $1
      RETURNING *`,
    [
      id,
      input.status,
      input.reason,
      {
        resolution_source: "admin",
        resolution_status: input.status,
        resolution_reason: input.reason,
        resolution_note: input.note || null,
        resolved_at: nowIso()
      }
    ]
  );

  return mapBatch(rows[0]);
}

export async function reprocessSettlementBatch(id: string): Promise<{
  source_batch: SettlementBatchEntity;
  new_batch: SettlementBatchEntity;
  moved_entry_count: number;
}> {
  return withClient(async (client) => {
    await client.query("BEGIN");
    try {
      const { rows: sourceRows } = await client.query("SELECT * FROM settlement_batches WHERE id = $1 FOR UPDATE", [id]);
      if (!sourceRows.length) {
        throw new Error("SETTLEMENT_BATCH_NOT_FOUND");
      }

      const source = mapBatch(sourceRows[0]);
      if (source.status !== "failed" && source.status !== "cancelled") {
        throw new Error("SETTLEMENT_BATCH_NOT_REPROCESSABLE");
      }
      if (source.metadata_json?.reprocessed_to_batch_id) {
        throw new Error("SETTLEMENT_BATCH_ALREADY_REPROCESSED");
      }

      const { rows: entryRows } = await client.query(
        "SELECT * FROM settlement_batch_entries WHERE batch_id = $1 ORDER BY created_at ASC FOR UPDATE",
        [id]
      );
      if (!entryRows.length) {
        throw new Error("SETTLEMENT_BATCH_EMPTY");
      }

      const newId = randomUUID();
      const now = nowIso();
      const { rows: newBatchRows } = await client.query(
        `INSERT INTO settlement_batches (
          id,
          batch_code,
          merchant_id,
          currency,
          gross_amount,
          platform_fee_amount,
          net_payable_amount,
          entry_count,
          status,
          cutoff_at,
          created_at,
          metadata_json
        ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)
        RETURNING *`,
        [
          newId,
          makeBatchCode(newId),
          source.merchant_id,
          source.currency,
          source.gross_amount,
          source.platform_fee_amount,
          source.net_payable_amount,
          entryRows.length,
          "created",
          now,
          now,
          {
            source: "settlement_reprocess",
            reprocessed_from_batch_id: source.id,
            reprocessed_from_batch_code: source.batch_code,
            previous_status: source.status
          }
        ]
      );

      await client.query(
        "UPDATE settlement_batch_entries SET batch_id = $2, created_at = $3 WHERE batch_id = $1",
        [id, newId, now]
      );

      const { rows: updatedSourceRows } = await client.query(
        `UPDATE settlement_batches
            SET metadata_json = metadata_json || $2::jsonb
          WHERE id = $1
          RETURNING *`,
        [
          id,
          {
            reprocessed_to_batch_id: newId,
            reprocessed_at: now
          }
        ]
      );

      await client.query("COMMIT");
      return {
        source_batch: mapBatch(updatedSourceRows[0]),
        new_batch: mapBatch(newBatchRows[0]),
        moved_entry_count: entryRows.length
      };
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    }
  });
}

export function toSettlementBatchPayload(batch: SettlementBatchEntity) {
  return { ...batch };
}

export function toSettlementBatchEntryPayload(entry: SettlementBatchEntryEntity) {
  return { ...entry };
}

export function toSettlementBatchAdjustmentPayload(adjustment: SettlementBatchAdjustmentEntity) {
  return { ...adjustment };
}

export function toSettlementBatchEventPayload(event: SettlementBatchEventEntity) {
  return { ...event };
}

function csvCell(value: unknown) {
  const text = value === null || value === undefined ? "" : String(value);
  return `"${text.replace(/"/g, '""')}"`;
}

export function settlementBatchReportToCsv(rows: SettlementBatchReportRow[]) {
  const headers = [
    "batch_code",
    "batch_status",
    "merchant_id",
    "merchant_name",
    "settlement_account_reference",
    "settlement_account_type",
    "transaction_id",
    "transaction_code",
    "partner_reference",
    "paid_at",
    "gross_amount",
    "platform_fee_amount",
    "net_payable_amount",
    "currency"
  ];

  const lines = rows.map((row) =>
    headers
      .map((header) => csvCell(row[header as keyof SettlementBatchReportRow]))
      .join(",")
  );

  return [headers.join(","), ...lines].join("\n") + "\n";
}

export function settlementBatchReportToBankGenericCsv(rows: SettlementBatchReportRow[]) {
  const headers = [
    "transfer_type",
    "beneficiary_account_reference",
    "beneficiary_account_type",
    "beneficiary_name",
    "amount",
    "currency",
    "remark",
    "batch_code",
    "transaction_count"
  ];

  if (!rows.length) {
    return headers.join(",") + "\n";
  }

  const first = rows[0];
  const totalAmount = rows.reduce((sum, row) => sum + Number(row.net_payable_amount || 0), 0);
  const line = [
    "merchant_payout",
    first.settlement_account_reference || "",
    first.settlement_account_type || "",
    first.merchant_name || first.merchant_id,
    Math.round((totalAmount + Number.EPSILON) * 100) / 100,
    first.currency || "IDR",
    `Settlement ${first.batch_code}`,
    first.batch_code,
    rows.length
  ];

  return [headers.join(","), line.map(csvCell).join(",")].join("\n") + "\n";
}

export function settlementAdjustmentReportToCsv(report: SettlementAdjustmentReport) {
  const headers = [
    "adjustment_id",
    "batch_id",
    "batch_code",
    "batch_status",
    "merchant_id",
    "adjustment_type",
    "amount",
    "signed_amount",
    "currency",
    "approval_status",
    "reason",
    "note",
    "actor_type",
    "actor_id",
    "batch_net_payable_amount",
    "batch_created_at",
    "batch_paid_at",
    "created_at"
  ];

  const lines = report.rows.map((row) =>
    [
      row.id,
      row.batch_id,
      row.batch_code,
      row.batch_status,
      row.merchant_id,
      row.adjustment_type,
      row.amount,
      row.signed_amount,
      row.currency,
      row.approval_status,
      row.reason,
      row.note || "",
      row.actor_type,
      row.actor_id || "",
      row.net_payable_amount,
      row.batch_created_at,
      row.batch_paid_at || "",
      row.created_at
    ]
      .map(csvCell)
      .join(",")
  );

  const summary = [
    ["generated_at", report.generated_at],
    ["total_count", report.total_count],
    ["credit_amount", report.credit_amount],
    ["debit_amount", report.debit_amount],
    ["signed_amount", report.signed_amount]
  ].map(([key, value]) => [key, value].map(csvCell).join(","));

  return [...summary, "", headers.join(","), ...lines].join("\n") + "\n";
}
