'use server'

import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase/server'
import { getAppOrg } from '@/lib/org'
import { getEffectiveUser } from '@/lib/effective-user'
import { canTransfer } from '@/lib/permissions'
import {
  findAutoRuleMatch,
  parseCcPaste,
  type AutoRuleMatch,
} from '@/lib/cc-expenses'

function fail(msg: string, retPath = '/tools/cc-expenses'): never {
  const params = new URLSearchParams({ error: msg })
  redirect(`${retPath}?${params.toString()}`)
}

/** Create a new CC expense batch. Owners only. */
export async function createCcBatch(formData: FormData) {
  const supabase = await createClient()
  const org = await getAppOrg(supabase)
  if (!org) fail('Organization not found.')

  const eu = await getEffectiveUser(supabase, org.id)
  if (!canTransfer(eu)) fail('Only an owner can create expense batches.')

  const name = String(formData.get('name') ?? '').trim()
  if (!name) fail('Batch name is required.')

  const { data, error } = await supabase
    .from('cc_expense_batches')
    .insert({
      org_id: org.id,
      name,
      created_by: eu?.real_user_id ?? null,
    })
    .select('id')
    .single<{ id: string }>()
  if (error || !data) fail(error?.message ?? 'Insert failed.')

  revalidatePath('/tools/cc-expenses')
  redirect(`/tools/cc-expenses/${data.id}`)
}

/**
 * Append parsed lines to a batch from a pasted block. Each non-empty
 * line is parsed and inserted as one cc_expense_lines row.
 * line_index continues from the current max so order is preserved
 * across multiple pastes.
 */
export async function appendCcLines(formData: FormData) {
  const batchId = String(formData.get('batch_id') ?? '').trim()
  if (!batchId) fail('Missing batch id.')

  const supabase = await createClient()
  const org = await getAppOrg(supabase)
  if (!org) fail('Organization not found.', `/tools/cc-expenses/${batchId}`)

  const eu = await getEffectiveUser(supabase, org.id)
  if (!canTransfer(eu)) {
    fail(
      'Only an owner can add expense lines.',
      `/tools/cc-expenses/${batchId}`,
    )
  }

  const pasted = String(formData.get('paste') ?? '')
  if (!pasted.trim()) {
    fail('Paste some lines first.', `/tools/cc-expenses/${batchId}`)
  }

  const parsed = parseCcPaste(pasted)
  if (parsed.length === 0) {
    fail(
      'No usable lines found in the paste.',
      `/tools/cc-expenses/${batchId}`,
    )
  }

  // Get the current max line_index so appends keep order.
  const { data: existing } = await supabase
    .from('cc_expense_lines')
    .select('line_index')
    .eq('batch_id', batchId)
    .order('line_index', { ascending: false })
    .limit(1)
    .maybeSingle<{ line_index: number }>()
  const startIdx = (existing?.line_index ?? -1) + 1

  // Pull auto-assign rules once, then match each line's description
  // against them in JS. Matched rules pre-fill (project_id, assignment_
  // description, category) on the inserted row — "first match wins" by
  // created_at desc (per decisions.md / tools-cc-expenses.md).
  const { data: rules } = await supabase
    .from('cc_auto_rules')
    .select('id, keyword, project_id, assignment_description, category')
    .eq('org_id', org.id)
    .order('created_at', { ascending: false })
    .returns<AutoRuleMatch[]>()
  const ruleList = rules ?? []

  let autoAssigned = 0
  const rows = parsed.map((p, i) => {
    const match = findAutoRuleMatch(p.description, ruleList)
    if (match) autoAssigned += 1
    return {
      batch_id: batchId,
      line_index: startIdx + i,
      raw_line: p.raw_line,
      expense_date: p.expense_date,
      description: p.description,
      amount_usd: p.amount_usd,
      balance_usd: p.balance_usd,
      project_id: match?.project_id ?? null,
      assignment_description: match?.assignment_description ?? null,
      category: match?.category ?? null,
      auto_assigned: match != null,
    }
  })

  const { error } = await supabase.from('cc_expense_lines').insert(rows)
  if (error) fail(error.message, `/tools/cc-expenses/${batchId}`)

  revalidatePath(`/tools/cc-expenses/${batchId}`)
  const info =
    autoAssigned > 0
      ? `Added ${parsed.length} lines (${autoAssigned} auto-assigned).`
      : `Added ${parsed.length} lines.`
  redirect(
    `/tools/cc-expenses/${batchId}?info=${encodeURIComponent(info)}`,
  )
}

/** Update a single line's assignment fields. */
export async function updateCcLine(formData: FormData) {
  const id = String(formData.get('id') ?? '').trim()
  const batchId = String(formData.get('batch_id') ?? '').trim()
  if (!id || !batchId) fail('Missing line or batch id.')

  const supabase = await createClient()
  const org = await getAppOrg(supabase)
  if (!org) fail('Organization not found.', `/tools/cc-expenses/${batchId}`)

  const eu = await getEffectiveUser(supabase, org.id)
  if (!canTransfer(eu)) {
    fail(
      'Only an owner can edit expense lines.',
      `/tools/cc-expenses/${batchId}`,
    )
  }

  const projectId =
    String(formData.get('project_id') ?? '').trim() || null
  const assignmentDescription =
    String(formData.get('assignment_description') ?? '').trim() || null
  const category = String(formData.get('category') ?? '').trim() || null

  const { error } = await supabase
    .from('cc_expense_lines')
    .update({
      project_id: projectId,
      assignment_description: assignmentDescription,
      category,
      // User saving the row = user has reviewed it. Flip out of the
      // "auto-assigned, needs review" state so the row paints green
      // (saved) instead of orange (auto-matched). See decisions.md
      // — tools-cc-expenses.md Phase 2 colors.
      auto_assigned: false,
    })
    .eq('id', id)
  if (error) fail(error.message, `/tools/cc-expenses/${batchId}`)

  revalidatePath(`/tools/cc-expenses/${batchId}`)
  // No redirect — let the form's server action complete inline so the
  // page refreshes with the new state in place. Server actions in
  // Next.js 15 trigger a revalidate by default; the explicit
  // revalidatePath above is belt-and-suspenders.
}

/** Delete a single line (e.g. it parsed wrong and should be removed). */
export async function deleteCcLine(formData: FormData) {
  const id = String(formData.get('id') ?? '').trim()
  const batchId = String(formData.get('batch_id') ?? '').trim()
  if (!id || !batchId) fail('Missing line or batch id.')

  const supabase = await createClient()
  const org = await getAppOrg(supabase)
  if (!org) fail('Organization not found.', `/tools/cc-expenses/${batchId}`)

  const eu = await getEffectiveUser(supabase, org.id)
  if (!canTransfer(eu)) {
    fail(
      'Only an owner can delete expense lines.',
      `/tools/cc-expenses/${batchId}`,
    )
  }

  const { error } = await supabase
    .from('cc_expense_lines')
    .delete()
    .eq('id', id)
  if (error) fail(error.message, `/tools/cc-expenses/${batchId}`)

  revalidatePath(`/tools/cc-expenses/${batchId}`)
}

/** Delete an entire batch (and its lines via FK cascade). */
export async function deleteCcBatch(formData: FormData) {
  const id = String(formData.get('id') ?? '').trim()
  if (!id) fail('Missing batch id.')

  const supabase = await createClient()
  const org = await getAppOrg(supabase)
  if (!org) fail('Organization not found.')

  const eu = await getEffectiveUser(supabase, org.id)
  if (!canTransfer(eu)) fail('Only an owner can delete batches.')

  const { error } = await supabase
    .from('cc_expense_batches')
    .delete()
    .eq('id', id)
  if (error) fail(error.message)

  revalidatePath('/tools/cc-expenses')
  redirect('/tools/cc-expenses?info=Batch+deleted.')
}

/**
 * Re-run auto-assign rules against UNASSIGNED lines in this batch.
 * Assigned lines (project_id IS NOT NULL) are left alone — they're
 * either user-confirmed or already auto-assigned from a previous run.
 *
 * Useful when:
 *   - The user added or edited a rule AFTER pasting a batch, and
 *     wants those new rules to apply retroactively to the unassigned
 *     lines they pasted before.
 *   - Lines that didn't match any rule originally need to be
 *     re-checked after the rule set grew.
 *
 * Performance: pulls all rules + all unassigned lines for the batch
 * once, matches in JS, then issues per-match UPDATE statements.
 * Batches of ~hundreds finish in a few hundred ms; fine for the
 * current scale. If batches grow into thousands, swap to a SQL
 * function with a JOIN.
 */
export async function reapplyRules(formData: FormData) {
  const batchId = String(formData.get('batch_id') ?? '').trim()
  if (!batchId) fail('Missing batch id.')

  const supabase = await createClient()
  const org = await getAppOrg(supabase)
  if (!org) fail('Organization not found.', `/tools/cc-expenses/${batchId}`)

  const eu = await getEffectiveUser(supabase, org.id)
  if (!canTransfer(eu)) {
    fail('Only an owner can re-apply rules.', `/tools/cc-expenses/${batchId}`)
  }

  const [{ data: rules }, { data: lines }] = await Promise.all([
    supabase
      .from('cc_auto_rules')
      .select('id, keyword, project_id, assignment_description, category')
      .eq('org_id', org.id)
      .order('created_at', { ascending: false })
      .returns<AutoRuleMatch[]>(),
    supabase
      .from('cc_expense_lines')
      .select('id, description')
      .eq('batch_id', batchId)
      .is('project_id', null)
      .returns<{ id: string; description: string | null }[]>(),
  ])

  const ruleList = rules ?? []
  const lineList = lines ?? []
  if (ruleList.length === 0 || lineList.length === 0) {
    revalidatePath(`/tools/cc-expenses/${batchId}`)
    redirect(
      `/tools/cc-expenses/${batchId}?info=${encodeURIComponent(
        ruleList.length === 0
          ? 'No auto-assign rules to apply.'
          : 'No unassigned lines to match.',
      )}`,
    )
  }

  let updated = 0
  for (const line of lineList) {
    const match = findAutoRuleMatch(line.description, ruleList)
    if (!match) continue
    const { error } = await supabase
      .from('cc_expense_lines')
      .update({
        project_id: match.project_id,
        assignment_description: match.assignment_description,
        category: match.category,
        auto_assigned: true,
      })
      .eq('id', line.id)
    if (error) fail(error.message, `/tools/cc-expenses/${batchId}`)
    updated += 1
  }

  revalidatePath(`/tools/cc-expenses/${batchId}`)
  redirect(
    `/tools/cc-expenses/${batchId}?info=${encodeURIComponent(
      updated === 0
        ? 'No new matches found.'
        : `Re-applied rules: ${updated} line${updated === 1 ? '' : 's'} auto-assigned.`,
    )}`,
  )
}
