/**
 * Pure helpers for the Tools / Bill Through Credit Card Expenses
 * feature. Pasted lines look like:
 *
 *   May 20, 2026<TAB>FACEBK *N7VKLP9RX2<TAB>$7.15<TAB><TAB>$3,856.56<TAB>
 *
 * Columns recognized (greedy): date, description, amount, credit?, balance?.
 * Trailing tabs are tolerated. Lines that don't have a date + description
 * + amount are still kept as raw rows so the user can see what failed.
 */

export type ParsedCcLine = {
  raw_line: string
  expense_date: string | null
  description: string | null
  amount_usd: number | null
  balance_usd: number | null
}

const MONTHS: Record<string, number> = {
  jan: 1, january: 1,
  feb: 2, february: 2,
  mar: 3, march: 3,
  apr: 4, april: 4,
  may: 5,
  jun: 6, june: 6,
  jul: 7, july: 7,
  aug: 8, august: 8,
  sep: 9, sept: 9, september: 9,
  oct: 10, october: 10,
  nov: 11, november: 11,
  dec: 12, december: 12,
}

/**
 * Parse a date string. Handles the long format that statements use
 * ("May 20, 2026") and the ISO format (YYYY-MM-DD). Returns
 * YYYY-MM-DD, or null on failure.
 */
export function parseExpenseDate(raw: string): string | null {
  const s = raw.trim()
  if (!s) return null
  // ISO YYYY-MM-DD
  const iso = s.match(/^(\d{4})-(\d{2})-(\d{2})$/)
  if (iso) return s
  // "May 20, 2026" / "May 20 2026" / "May 20, 2026,"
  const long = s.match(/^([A-Za-z]+)\.?\s+(\d{1,2}),?\s+(\d{4})$/)
  if (long) {
    const mon = MONTHS[long[1].toLowerCase()]
    if (!mon) return null
    const day = Number(long[2])
    const year = Number(long[3])
    if (day < 1 || day > 31 || year < 1900 || year > 2999) return null
    return (
      `${year.toString().padStart(4, '0')}-` +
      `${mon.toString().padStart(2, '0')}-` +
      `${day.toString().padStart(2, '0')}`
    )
  }
  return null
}

/**
 * Parse a currency string like "$7.15" or "$3,856.56" or "7.15" into
 * a Number. Returns null on failure or for an empty input. Doesn't
 * preserve sign (assumes positive — the caller decides debit/credit).
 */
export function parseCurrency(raw: string): number | null {
  const s = raw.trim().replace(/^\$/, '').replace(/,/g, '')
  if (!s) return null
  const n = Number(s)
  if (!Number.isFinite(n)) return null
  return Math.round(n * 100) / 100
}

/**
 * Parse one pasted line into the column slots we recognize. Returns
 * the parsed row even when individual fields fail (NULL fields), so
 * the user can see the row in the UI and fix it.
 *
 * Tab-separated columns expected (extra trailing tabs ignored):
 *   1. date (e.g. "May 20, 2026")
 *   2. description
 *   3. amount (debit) — e.g. "$7.15"
 *   4. credit — usually empty, treated as alt amount if debit is blank
 *   5. balance — running balance after this transaction
 *
 * Empty input → null (caller filters out).
 */
export function parseCcLine(raw: string): ParsedCcLine | null {
  if (!raw.trim()) return null
  const cols = raw.split('\t').map((c) => c.trim())
  const date = cols[0] ? parseExpenseDate(cols[0]) : null
  const description = cols[1]?.trim() || null
  const debit = cols[2] ? parseCurrency(cols[2]) : null
  const credit = cols[3] ? parseCurrency(cols[3]) : null
  // Use whichever amount column is set. If both, debit wins.
  const amount = debit ?? credit ?? null
  const balance = cols[4] ? parseCurrency(cols[4]) : null
  return {
    raw_line: raw,
    expense_date: date,
    description,
    amount_usd: amount,
    balance_usd: balance,
  }
}

/**
 * Parse a multi-line paste. Splits on newlines, skips blanks, returns
 * one ParsedCcLine per non-empty line in order.
 */
export function parseCcPaste(input: string): ParsedCcLine[] {
  return input
    .split(/\r?\n/)
    .map((line) => parseCcLine(line))
    .filter((l): l is ParsedCcLine => l !== null)
}

/**
 * The export shape — one assigned line in the destination PlusROI
 * sheet. Mirrors the invoice paste block (lib/invoices.ts >
 * formatPasteBlock) but uses the CC expense's own assignment fields.
 */
export type AssignedCcLineForPaste = {
  client: string
  project: string
  assignment_description: string
  amount_usd: number
  expense_date: string
  category: string
}

/**
 * Tab-separated paste block for the legacy PlusROI sheet.
 *
 * Per line:
 *   {client} : {project}<TAB>{assignment_description}<TAB>{amount}<TAB>{date}<TAB><TAB>Bowden Works<TAB>{category}
 *
 * Same 7-column shape as the invoice paste block (lib/invoices.ts);
 * column 2 holds the CC-line description ("Google", "Facebook", etc.)
 * instead of the fixed "Bowden Works Team" string, and column 7 is
 * the CC expense category instead of "Labour". Column 5 stays empty;
 * column 6 stays "Bowden Works".
 */
/**
 * Highlight keyword: anything with a non-blank `keyword` string. The
 * matcher does case-insensitive substring contains against a line's
 * description. Returns the matched keyword (the first one to hit, in
 * the order passed) or null if nothing matched. A null/empty
 * description never matches.
 */
export function findHighlightMatch(
  description: string | null,
  keywords: { keyword: string }[],
): string | null {
  if (!description) return null
  const d = description.toLowerCase()
  for (const k of keywords) {
    if (!k.keyword) continue
    if (d.includes(k.keyword.toLowerCase())) return k.keyword
  }
  return null
}

/** Shape of an auto-assign rule the matcher cares about. */
export type AutoRuleMatch = {
  id: string
  keyword: string
  project_id: string
  assignment_description: string | null
  category: string | null
}

/**
 * Auto-assign rule matcher. Same case-insensitive substring rule as
 * highlight keywords, but the matched rule's assignment fields (project,
 * description, category) get returned so the caller can stamp them onto
 * the inserted line. "First match wins" — caller controls order by
 * passing a sorted list (created_at desc in practice). Returns null if
 * nothing matched.
 */
export function findAutoRuleMatch(
  description: string | null,
  rules: AutoRuleMatch[],
): AutoRuleMatch | null {
  if (!description) return null
  const d = description.toLowerCase()
  for (const r of rules) {
    if (!r.keyword) continue
    if (d.includes(r.keyword.toLowerCase())) return r
  }
  return null
}

export function formatCcPasteBlock(lines: AssignedCcLineForPaste[]): string {
  return lines
    .map((l) =>
      [
        `${l.client} : ${l.project}`,
        l.assignment_description,
        l.amount_usd.toFixed(2),
        l.expense_date,
        '',
        'Bowden Works',
        l.category,
      ].join('\t'),
    )
    .join('\n')
}
