import {
  pgAddBankReconciliationDifference,
  pgCreateReconciliationMatch,
  pgGetBankReconciliationDifferencesForPeriod,
  pgGetBankReconciliationPeriodById,
  pgGetBankTransactionsForPeriod,
  pgGetLedgerTransactionsForBankAccount,
  pgGetReconciliationMatchForBankTransaction,
  pgUpdateLedgerTransactionReconciliationStatus,
} from '@/lib/postgres/bank-reconciliation';
import type {
  BankDifferenceType,
  BankTransaction,
  LedgerTransaction,
  ReconciliationMatch,
} from '@/lib/types';

export type AutoMatchResult = {
  periodId: string;
  totalBankTransactions: number;
  candidateLedgerTransactions: number;
  matchedCount: number;
  unmatchedBankCount: number;
  unmatchedLedgerCount: number;
  matches: ReconciliationMatch[];
};

export type AutoMatchPreviewSuggestion = {
  bankTransactionId: string;
  ledgerTransactionId: string;
  matchType: 'auto' | 'partial';
  matchConfidence: number;
  scoreBreakdown: {
    dateDaysDifference: number;
    amountDifference: number;
    amountDifferencePercent: number;
    directionMatches: boolean;
  };
};

export type AutoMatchPreviewDifference = {
  type: BankDifferenceType;
  amount: number;
  bankTransactionId?: string;
  ledgerTransactionId?: string;
  description: string;
};

export type AutoMatchPreviewResult = {
  periodId: string;
  totalBankTransactions: number;
  candidateLedgerTransactions: number;
  threshold: number;
  suggestions: AutoMatchPreviewSuggestion[];
  unmatchedBank: AutoMatchPreviewDifference[];
  unmatchedLedger: AutoMatchPreviewDifference[];
};

export type ApplyPreviewInputSuggestion = {
  bankTransactionId: string;
  ledgerTransactionId: string;
  matchConfidence?: number;
  matchType?: 'auto' | 'partial';
};

export type ApplyAutoMatchPreviewResult = AutoMatchResult & {
  appliedCount: number;
  skippedCount: number;
  skippedReasons: Array<{
    bankTransactionId: string;
    ledgerTransactionId: string;
    reason: string;
  }>;
};

export type ApplyAutoMatchPreviewDryRunResult = {
  dryRun: true;
  periodId: string;
  totalBankTransactions: number;
  candidateLedgerTransactions: number;
  matchedCount: number;
  unmatchedBankCount: number;
  unmatchedLedgerCount: number;
  appliedCount: number;
  skippedCount: number;
  wouldApply: Array<{
    bankTransactionId: string;
    ledgerTransactionId: string;
    matchType: 'auto' | 'partial';
    matchConfidence: number;
  }>;
  skippedReasons: Array<{
    bankTransactionId: string;
    ledgerTransactionId: string;
    reason: string;
  }>;
};

function normalizeDirection(value?: string): 'in' | 'out' | 'unknown' {
  if (!value) return 'unknown';
  const v = value.toLowerCase();
  if (v === 'inward' || v === 'debit') return 'in';
  if (v === 'outward' || v === 'credit') return 'out';
  return 'unknown';
}

function dayDiff(dateA: string, dateB: string): number {
  const a = new Date(dateA);
  const b = new Date(dateB);
  const msPerDay = 24 * 60 * 60 * 1000;
  return Math.round(Math.abs(a.getTime() - b.getTime()) / msPerDay);
}

function computeConfidence(bankTxn: BankTransaction, ledgerTxn: LedgerTransaction): number {
  let score = 50;

  const days = dayDiff(bankTxn.transactionDate, ledgerTxn.transactionDate);
  if (days === 0) score += 30;
  else if (days <= 1) score += 20;
  else if (days <= 3) score += 10;

  const amountDiff = Math.abs(bankTxn.amount - ledgerTxn.amount);
  const maxAmount = Math.max(Math.abs(bankTxn.amount), Math.abs(ledgerTxn.amount), 1);
  const diffPercent = (amountDiff / maxAmount) * 100;

  if (amountDiff === 0) score += 25;
  else if (diffPercent <= 1) score += 20;
  else if (diffPercent <= 5) score += 10;

  const bankDirection = normalizeDirection(bankTxn.direction);
  const ledgerDirection = normalizeDirection(ledgerTxn.direction);
  if (bankDirection !== 'unknown' && ledgerDirection !== 'unknown' && bankDirection === ledgerDirection) {
    score += 10;
  }

  const bankDesc = (bankTxn.description || '').toLowerCase();
  const ledgerDesc = (ledgerTxn.description || '').toLowerCase();
  if (bankDesc && ledgerDesc) {
    const bankWords = bankDesc.split(/\s+/).filter((w) => w.length > 2);
    const overlap = bankWords.some((w) => ledgerDesc.includes(w));
    if (overlap) score += 10;
  }

  return Math.max(0, Math.min(100, score));
}

function buildDifferenceTypeForBank(bankTxn: BankTransaction): BankDifferenceType {
  const direction = normalizeDirection(bankTxn.direction);
  return direction === 'out' ? 'outstanding_check' : 'unmatched_bank_transaction';
}

async function addResidualDifferences(
  periodId: string,
  bankTransactions: BankTransaction[],
  unmatchedBankIds: Set<string>,
  availableLedger: Map<string, LedgerTransaction>
): Promise<void> {
  const previousDifferences = await pgGetBankReconciliationDifferencesForPeriod(periodId);
  const existingBankDiffIds = new Set(
    previousDifferences
      .filter((d) => d.bankTransactionId)
      .map((d) => `${d.type}:${d.bankTransactionId}`)
  );
  const existingLedgerDiffIds = new Set(
    previousDifferences
      .filter((d) => d.ledgerTransactionId)
      .map((d) => `${d.type}:${d.ledgerTransactionId}`)
  );

  for (const bankTxn of bankTransactions) {
    if (!unmatchedBankIds.has(bankTxn.id)) continue;

    const type = buildDifferenceTypeForBank(bankTxn);
    const key = `${type}:${bankTxn.id}`;
    if (existingBankDiffIds.has(key)) continue;

    await pgAddBankReconciliationDifference({
      reconciliationPeriodId: periodId,
      type,
      amount: bankTxn.amount,
      description: bankTxn.description || 'Unmatched bank transaction',
      bankTransactionId: bankTxn.id,
      resolutionType: 'pending',
      notes: 'Created by auto-match engine',
    });
  }

  for (const ledgerTxn of availableLedger.values()) {
    const key = `unmatched_ledger_transaction:${ledgerTxn.id}`;
    if (existingLedgerDiffIds.has(key)) continue;

    await pgAddBankReconciliationDifference({
      reconciliationPeriodId: periodId,
      type: 'unmatched_ledger_transaction',
      amount: ledgerTxn.amount,
      description: ledgerTxn.description || 'Unmatched ledger transaction',
      ledgerTransactionId: ledgerTxn.id,
      resolutionType: 'pending',
      notes: 'Created by auto-match engine',
    });
  }
}

export async function previewAutoMatchForPeriod(
  periodId: string,
  threshold = 80
): Promise<AutoMatchPreviewResult> {
  const period = await pgGetBankReconciliationPeriodById(periodId);
  if (!period) {
    throw new Error('Reconciliation period not found');
  }

  const bankTransactions = await pgGetBankTransactionsForPeriod(periodId);
  const ledgerTransactions = await pgGetLedgerTransactionsForBankAccount(period.bankAccountId);

  const availableLedger = new Map<string, LedgerTransaction>(
    ledgerTransactions.map((txn) => [txn.id, txn])
  );

  const suggestions: AutoMatchPreviewSuggestion[] = [];

  for (const bankTxn of bankTransactions) {
    const alreadyMatched = await pgGetReconciliationMatchForBankTransaction(bankTxn.id);
    if (alreadyMatched) {
      continue;
    }

    let bestCandidate: LedgerTransaction | null = null;
    let bestScore = -1;

    for (const ledgerTxn of availableLedger.values()) {
      const amountDiff = Math.abs(bankTxn.amount - ledgerTxn.amount);
      const maxAmount = Math.max(Math.abs(bankTxn.amount), Math.abs(ledgerTxn.amount), 1);
      const diffPercent = (amountDiff / maxAmount) * 100;
      const days = dayDiff(bankTxn.transactionDate, ledgerTxn.transactionDate);

      if (days > 7 || diffPercent > 5) {
        continue;
      }

      const score = computeConfidence(bankTxn, ledgerTxn);
      if (score > bestScore) {
        bestScore = score;
        bestCandidate = ledgerTxn;
      }
    }

    if (bestCandidate && bestScore >= threshold) {
      const amountDifference = Math.abs(bankTxn.amount - bestCandidate.amount);
      const maxAmount = Math.max(Math.abs(bankTxn.amount), Math.abs(bestCandidate.amount), 1);
      suggestions.push({
        bankTransactionId: bankTxn.id,
        ledgerTransactionId: bestCandidate.id,
        matchType: bestScore >= 95 ? 'auto' : 'partial',
        matchConfidence: bestScore,
        scoreBreakdown: {
          dateDaysDifference: dayDiff(bankTxn.transactionDate, bestCandidate.transactionDate),
          amountDifference,
          amountDifferencePercent: (amountDifference / maxAmount) * 100,
          directionMatches:
            normalizeDirection(bankTxn.direction) !== 'unknown' &&
            normalizeDirection(bestCandidate.direction) !== 'unknown' &&
            normalizeDirection(bankTxn.direction) === normalizeDirection(bestCandidate.direction),
        },
      });

      availableLedger.delete(bestCandidate.id);
    }
  }

  const matchedBankIds = new Set(suggestions.map((s) => s.bankTransactionId));

  const unmatchedBank: AutoMatchPreviewDifference[] = bankTransactions
    .filter((txn) => !matchedBankIds.has(txn.id))
    .map((txn) => ({
      type: buildDifferenceTypeForBank(txn),
      amount: txn.amount,
      bankTransactionId: txn.id,
      description: txn.description || 'Unmatched bank transaction',
    }));

  const unmatchedLedger: AutoMatchPreviewDifference[] = Array.from(availableLedger.values()).map((txn) => ({
    type: 'unmatched_ledger_transaction',
    amount: txn.amount,
    ledgerTransactionId: txn.id,
    description: txn.description || 'Unmatched ledger transaction',
  }));

  return {
    periodId,
    totalBankTransactions: bankTransactions.length,
    candidateLedgerTransactions: ledgerTransactions.length,
    threshold,
    suggestions,
    unmatchedBank,
    unmatchedLedger,
  };
}

export async function runAutoMatchForPeriod(
  periodId: string,
  matchedBy: string,
  threshold = 80
): Promise<AutoMatchResult> {
  const period = await pgGetBankReconciliationPeriodById(periodId);
  if (!period) {
    throw new Error('Reconciliation period not found');
  }

  const bankTransactions = await pgGetBankTransactionsForPeriod(periodId);
  const ledgerTransactions = await pgGetLedgerTransactionsForBankAccount(period.bankAccountId);

  const availableLedger = new Map<string, LedgerTransaction>(
    ledgerTransactions.map((txn) => [txn.id, txn])
  );

  const createdMatches: ReconciliationMatch[] = [];
  const alreadyMatchedBankIds = new Set<string>();

  for (const bankTxn of bankTransactions) {
    const alreadyMatched = await pgGetReconciliationMatchForBankTransaction(bankTxn.id);
    if (alreadyMatched) {
      alreadyMatchedBankIds.add(bankTxn.id);
      continue;
    }

    let bestCandidate: LedgerTransaction | null = null;
    let bestScore = -1;

    for (const ledgerTxn of availableLedger.values()) {
      const amountDiff = Math.abs(bankTxn.amount - ledgerTxn.amount);
      const maxAmount = Math.max(Math.abs(bankTxn.amount), Math.abs(ledgerTxn.amount), 1);
      const diffPercent = (amountDiff / maxAmount) * 100;
      const days = dayDiff(bankTxn.transactionDate, ledgerTxn.transactionDate);

      if (days > 7 || diffPercent > 5) {
        continue;
      }

      const score = computeConfidence(bankTxn, ledgerTxn);
      if (score > bestScore) {
        bestScore = score;
        bestCandidate = ledgerTxn;
      }
    }

    if (bestCandidate && bestScore >= threshold) {
      const matchType = bestScore >= 95 ? 'auto' : 'partial';
      const match = await pgCreateReconciliationMatch({
        reconciliationPeriodId: periodId,
        bankTransactionId: bankTxn.id,
        ledgerTransactionIds: [bestCandidate.id],
        matchType,
        matchConfidence: bestScore,
        matchDate: new Date().toISOString(),
        matchedBy,
        notes: `Auto-match score ${bestScore}`,
        status: 'matched',
      });

      await pgUpdateLedgerTransactionReconciliationStatus(
        bestCandidate.id,
        'reconciled',
        periodId
      );

      createdMatches.push(match);
      availableLedger.delete(bestCandidate.id);
    }
  }

  const matchedBankTransactionIds = new Set(createdMatches.map((m) => m.bankTransactionId));
  const unmatchedBankIds = new Set(
    bankTransactions
      .map((txn) => txn.id)
      .filter((id) => !matchedBankTransactionIds.has(id) && !alreadyMatchedBankIds.has(id))
  );

  await addResidualDifferences(periodId, bankTransactions, unmatchedBankIds, availableLedger);

  return {
    periodId,
    totalBankTransactions: bankTransactions.length,
    candidateLedgerTransactions: ledgerTransactions.length,
    matchedCount: createdMatches.length,
    unmatchedBankCount: bankTransactions.length - createdMatches.length,
    unmatchedLedgerCount: availableLedger.size,
    matches: createdMatches,
  };
}

export async function applyAutoMatchPreviewForPeriod(
  periodId: string,
  matchedBy: string,
  suggestions: ApplyPreviewInputSuggestion[]
): Promise<ApplyAutoMatchPreviewResult> {
  const period = await pgGetBankReconciliationPeriodById(periodId);
  if (!period) {
    throw new Error('Reconciliation period not found');
  }

  const bankTransactions = await pgGetBankTransactionsForPeriod(periodId);
  const ledgerTransactions = await pgGetLedgerTransactionsForBankAccount(period.bankAccountId);

  const bankMap = new Map<string, BankTransaction>(bankTransactions.map((txn) => [txn.id, txn]));
  const availableLedger = new Map<string, LedgerTransaction>(
    ledgerTransactions.map((txn) => [txn.id, txn])
  );

  const createdMatches: ReconciliationMatch[] = [];
  const skippedReasons: Array<{
    bankTransactionId: string;
    ledgerTransactionId: string;
    reason: string;
  }> = [];

  const usedBankIds = new Set<string>();
  const usedLedgerIds = new Set<string>();
  const alreadyMatchedBankIds = new Set<string>();

  for (const suggestion of suggestions) {
    const bankTxn = bankMap.get(suggestion.bankTransactionId);
    const ledgerTxn = availableLedger.get(suggestion.ledgerTransactionId);

    if (!bankTxn) {
      skippedReasons.push({
        bankTransactionId: suggestion.bankTransactionId,
        ledgerTransactionId: suggestion.ledgerTransactionId,
        reason: 'Bank transaction not found in this period',
      });
      continue;
    }

    const alreadyMatched = await pgGetReconciliationMatchForBankTransaction(bankTxn.id);
    if (alreadyMatched) {
      alreadyMatchedBankIds.add(bankTxn.id);
      skippedReasons.push({
        bankTransactionId: bankTxn.id,
        ledgerTransactionId: suggestion.ledgerTransactionId,
        reason: 'Bank transaction is already matched',
      });
      continue;
    }

    if (!ledgerTxn) {
      skippedReasons.push({
        bankTransactionId: bankTxn.id,
        ledgerTransactionId: suggestion.ledgerTransactionId,
        reason: 'Ledger transaction not available or already reconciled',
      });
      continue;
    }

    if (usedBankIds.has(bankTxn.id)) {
      skippedReasons.push({
        bankTransactionId: bankTxn.id,
        ledgerTransactionId: ledgerTxn.id,
        reason: 'Duplicate bank transaction in request',
      });
      continue;
    }

    if (usedLedgerIds.has(ledgerTxn.id)) {
      skippedReasons.push({
        bankTransactionId: bankTxn.id,
        ledgerTransactionId: ledgerTxn.id,
        reason: 'Duplicate ledger transaction in request',
      });
      continue;
    }

    const computedScore = computeConfidence(bankTxn, ledgerTxn);
    const matchType = suggestion.matchType || (computedScore >= 95 ? 'auto' : 'partial');

    const match = await pgCreateReconciliationMatch({
      reconciliationPeriodId: periodId,
      bankTransactionId: bankTxn.id,
      ledgerTransactionIds: [ledgerTxn.id],
      matchType,
      matchConfidence: suggestion.matchConfidence ?? computedScore,
      matchDate: new Date().toISOString(),
      matchedBy,
      notes: `Applied from preview with score ${computedScore}`,
      status: 'matched',
    });

    await pgUpdateLedgerTransactionReconciliationStatus(ledgerTxn.id, 'reconciled', periodId);

    createdMatches.push(match);
    usedBankIds.add(bankTxn.id);
    usedLedgerIds.add(ledgerTxn.id);
    availableLedger.delete(ledgerTxn.id);
  }

  const matchedBankTransactionIds = new Set(createdMatches.map((m) => m.bankTransactionId));
  const unmatchedBankIds = new Set(
    bankTransactions
      .map((txn) => txn.id)
      .filter((id) => !matchedBankTransactionIds.has(id) && !alreadyMatchedBankIds.has(id))
  );

  await addResidualDifferences(periodId, bankTransactions, unmatchedBankIds, availableLedger);

  return {
    periodId,
    totalBankTransactions: bankTransactions.length,
    candidateLedgerTransactions: ledgerTransactions.length,
    matchedCount: createdMatches.length,
    unmatchedBankCount: unmatchedBankIds.size,
    unmatchedLedgerCount: availableLedger.size,
    matches: createdMatches,
    appliedCount: createdMatches.length,
    skippedCount: skippedReasons.length,
    skippedReasons,
  };
}

export async function applyAutoMatchFromPreviewForPeriod(
  periodId: string,
  matchedBy: string,
  threshold = 80
): Promise<ApplyAutoMatchPreviewResult> {
  const preview = await previewAutoMatchForPeriod(periodId, threshold);

  const suggestions: ApplyPreviewInputSuggestion[] = preview.suggestions.map((s) => ({
    bankTransactionId: s.bankTransactionId,
    ledgerTransactionId: s.ledgerTransactionId,
    matchType: s.matchType,
    matchConfidence: s.matchConfidence,
  }));

  return applyAutoMatchPreviewForPeriod(periodId, matchedBy, suggestions);
}

export async function dryRunApplyAutoMatchPreviewForPeriod(
  periodId: string,
  suggestions: ApplyPreviewInputSuggestion[]
): Promise<ApplyAutoMatchPreviewDryRunResult> {
  const period = await pgGetBankReconciliationPeriodById(periodId);
  if (!period) {
    throw new Error('Reconciliation period not found');
  }

  const bankTransactions = await pgGetBankTransactionsForPeriod(periodId);
  const ledgerTransactions = await pgGetLedgerTransactionsForBankAccount(period.bankAccountId);

  const bankMap = new Map<string, BankTransaction>(bankTransactions.map((txn) => [txn.id, txn]));
  const availableLedger = new Map<string, LedgerTransaction>(
    ledgerTransactions.map((txn) => [txn.id, txn])
  );

  const wouldApply: Array<{
    bankTransactionId: string;
    ledgerTransactionId: string;
    matchType: 'auto' | 'partial';
    matchConfidence: number;
  }> = [];

  const skippedReasons: Array<{
    bankTransactionId: string;
    ledgerTransactionId: string;
    reason: string;
  }> = [];

  const usedBankIds = new Set<string>();
  const usedLedgerIds = new Set<string>();
  const alreadyMatchedBankIds = new Set<string>();

  for (const suggestion of suggestions) {
    const bankTxn = bankMap.get(suggestion.bankTransactionId);
    const ledgerTxn = availableLedger.get(suggestion.ledgerTransactionId);

    if (!bankTxn) {
      skippedReasons.push({
        bankTransactionId: suggestion.bankTransactionId,
        ledgerTransactionId: suggestion.ledgerTransactionId,
        reason: 'Bank transaction not found in this period',
      });
      continue;
    }

    const alreadyMatched = await pgGetReconciliationMatchForBankTransaction(bankTxn.id);
    if (alreadyMatched) {
      alreadyMatchedBankIds.add(bankTxn.id);
      skippedReasons.push({
        bankTransactionId: bankTxn.id,
        ledgerTransactionId: suggestion.ledgerTransactionId,
        reason: 'Bank transaction is already matched',
      });
      continue;
    }

    if (!ledgerTxn) {
      skippedReasons.push({
        bankTransactionId: bankTxn.id,
        ledgerTransactionId: suggestion.ledgerTransactionId,
        reason: 'Ledger transaction not available or already reconciled',
      });
      continue;
    }

    if (usedBankIds.has(bankTxn.id)) {
      skippedReasons.push({
        bankTransactionId: bankTxn.id,
        ledgerTransactionId: ledgerTxn.id,
        reason: 'Duplicate bank transaction in request',
      });
      continue;
    }

    if (usedLedgerIds.has(ledgerTxn.id)) {
      skippedReasons.push({
        bankTransactionId: bankTxn.id,
        ledgerTransactionId: ledgerTxn.id,
        reason: 'Duplicate ledger transaction in request',
      });
      continue;
    }

    const computedScore = computeConfidence(bankTxn, ledgerTxn);
    const matchType = suggestion.matchType || (computedScore >= 95 ? 'auto' : 'partial');

    wouldApply.push({
      bankTransactionId: bankTxn.id,
      ledgerTransactionId: ledgerTxn.id,
      matchType,
      matchConfidence: suggestion.matchConfidence ?? computedScore,
    });

    usedBankIds.add(bankTxn.id);
    usedLedgerIds.add(ledgerTxn.id);
    availableLedger.delete(ledgerTxn.id);
  }

  const matchedBankTransactionIds = new Set(wouldApply.map((m) => m.bankTransactionId));
  const unmatchedBankIds = new Set(
    bankTransactions
      .map((txn) => txn.id)
      .filter((id) => !matchedBankTransactionIds.has(id) && !alreadyMatchedBankIds.has(id))
  );

  return {
    dryRun: true,
    periodId,
    totalBankTransactions: bankTransactions.length,
    candidateLedgerTransactions: ledgerTransactions.length,
    matchedCount: wouldApply.length,
    unmatchedBankCount: unmatchedBankIds.size,
    unmatchedLedgerCount: availableLedger.size,
    appliedCount: wouldApply.length,
    skippedCount: skippedReasons.length,
    wouldApply,
    skippedReasons,
  };
}

export async function dryRunApplyAutoMatchFromPreviewForPeriod(
  periodId: string,
  threshold = 80
): Promise<ApplyAutoMatchPreviewDryRunResult> {
  const preview = await previewAutoMatchForPeriod(periodId, threshold);

  const suggestions: ApplyPreviewInputSuggestion[] = preview.suggestions.map((s) => ({
    bankTransactionId: s.bankTransactionId,
    ledgerTransactionId: s.ledgerTransactionId,
    matchType: s.matchType,
    matchConfidence: s.matchConfidence,
  }));

  return dryRunApplyAutoMatchPreviewForPeriod(periodId, suggestions);
}
