'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 { canManageImports } from '@/lib/permissions'
import {
  applyConversion,
  computeBilloutAmount,
  type ParsedEntry,
  type TeamLookup,
} from '@/lib/clockify'
import { getEffectiveBilloutRate, getTeamForUser } from '@/lib/teams'

function fail(msg: string, pendingId?: string): never {
  const qs = new URLSearchParams({ error: msg })
  if (pendingId) qs.set('id', pendingId)
  redirect(`/import/resolve?${qs.toString()}`)
}

/**
 * Read the user's resolution for one unknown name. The form encodes
 * each decision as `<scope>_decision_<index>` with value `create` or
 * `map:<existing-uuid>`. We pair that with the originally-rendered
 * `<scope>_name_<index>` so we know which CSV name we're resolving.
 */
type Decision =
  | { kind: 'create'; name: string }
  | { kind: 'map'; toId: string; name: string }

function readDecisions(
  formData: FormData,
  scope: 'op' | 'cl' | 'pr',
): Map<string, Decision> {
  const out = new Map<string, Decision>()
  // Find indices. We rendered N inputs with keys `${scope}_name_<i>` and `${scope}_decision_<i>`.
  for (const [k, v] of formData.entries()) {
    const m = k.match(new RegExp(`^${scope}_name_(\\d+)$`))
    if (!m) continue
    const idx = m[1]
    const name = String(v)
    const decision = String(formData.get(`${scope}_decision_${idx}`) ?? 'create')
    if (decision.startsWith('map:')) {
      out.set(name, { kind: 'map', toId: decision.slice(4), name })
    } else {
      out.set(name, { kind: 'create', name })
    }
  }
  return out
}

export async function commitPendingImport(formData: FormData) {
  const pendingId = String(formData.get('pending_id') ?? '')
  if (!pendingId) fail('Missing pending import id.')

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

  const eu = await getEffectiveUser(supabase, org.id)
  if (!eu || !canManageImports(eu)) {
    fail('You do not have permission to import.', pendingId)
  }

  // Load the pending row. RLS already gates this.
  type PendingRow = {
    id: string
    org_id: string
    uploaded_by: string
    filename: string | null
    batch_name: string | null
    notes: string | null
    source: 'clockify' | 'toggl'
    parsed_entries: ParsedEntry[]
  }
  const { data: pending, error: loadErr } = await supabase
    .from('pending_imports')
    .select(
      'id, org_id, uploaded_by, filename, batch_name, notes, source, parsed_entries',
    )
    .eq('id', pendingId)
    .maybeSingle<PendingRow>()
  if (loadErr) fail(loadErr.message, pendingId)
  if (!pending) fail('Pending import not found (may have expired).', pendingId)

  const parsedEntries = pending.parsed_entries ?? []

  // Read decisions for every unknown name surfaced on the resolver page.
  const opDecisions = readDecisions(formData, 'op')
  const clDecisions = readDecisions(formData, 'cl')
  const prDecisions = readDecisions(formData, 'pr')

  // Load existing entities so we can lookup by lowercased name.
  const { data: existingOperators } = await supabase
    .from('operators')
    .select('id, name')
    .eq('org_id', org.id)
    .returns<{ id: string; name: string }[]>()
  const opByLowerName = new Map(
    (existingOperators ?? []).map((o) => [o.name.toLowerCase(), o]),
  )

  // Step 1: resolve all operator names. Existing-match wins; unknowns
  // use the user's decision (create new or map to existing).
  const opResolved = new Map<string, string>() // raw name → operator_id
  const opCanonical = new Map<string, string>() // raw name → canonical name (post-mapping)
  const distinctOperators = new Set(
    parsedEntries.map((e) => e.operator).filter((o): o is string => !!o),
  )
  for (const name of distinctOperators) {
    const existing = opByLowerName.get(name.toLowerCase())
    if (existing) {
      opResolved.set(name, existing.id)
      opCanonical.set(name, existing.name)
      continue
    }
    const decision = opDecisions.get(name)
    if (!decision) {
      fail(
        `No decision provided for unknown operator "${name}". Please re-submit the resolver.`,
        pendingId,
      )
    }
    if (decision.kind === 'map') {
      const target = (existingOperators ?? []).find((o) => o.id === decision.toId)
      if (!target) fail(`Mapped operator id not found.`, pendingId)
      opResolved.set(name, target.id)
      opCanonical.set(name, target.name)
    } else {
      const { data: newOp, error } = await supabase
        .from('operators')
        .insert({ org_id: org.id, name })
        .select('id, name')
        .single<{ id: string; name: string }>()
      if (error || !newOp) fail(`Failed to create operator "${name}": ${error?.message}`, pendingId)
      opResolved.set(name, newOp.id)
      opCanonical.set(name, newOp.name)
    }
  }

  // Step 2: clients. Key is (resolved-operator-id, raw client name).
  const { data: existingClients } = await supabase
    .from('clients')
    .select('id, operator_id, name')
    .eq('org_id', org.id)
    .returns<{ id: string; operator_id: string; name: string }[]>()
  const clByOpAndLowerName = new Map<string, { id: string; name: string }>()
  for (const c of existingClients ?? []) {
    clByOpAndLowerName.set(`${c.operator_id}|${c.name.toLowerCase()}`, c)
  }

  // (operator-raw-name, client-raw-name) → client_id
  const clResolved = new Map<string, string>()
  const clCanonical = new Map<string, string>()
  const distinctOpClient = new Set<string>()
  for (const e of parsedEntries) {
    if (e.operator && e.client) distinctOpClient.add(`${e.operator}|${e.client}`)
  }
  for (const opClientKey of distinctOpClient) {
    const [opName, clName] = opClientKey.split('|', 2)
    const opId = opResolved.get(opName)
    if (!opId) {
      fail(
        `Operator "${opName}" wasn't resolved (internal bug). Re-submit the resolver.`,
        pendingId,
      )
    }
    const existing = clByOpAndLowerName.get(`${opId}|${clName.toLowerCase()}`)
    if (existing) {
      clResolved.set(opClientKey, existing.id)
      clCanonical.set(opClientKey, existing.name)
      continue
    }
    // Decision lookup uses a stable key: "<opName>|<clName>" — same as
    // the resolver page rendered.
    const decisionKey = opClientKey
    const decision = clDecisions.get(decisionKey)
    if (!decision) {
      fail(
        `No decision for unknown client "${clName}" under operator "${opName}".`,
        pendingId,
      )
    }
    if (decision.kind === 'map') {
      const target = (existingClients ?? []).find((c) => c.id === decision.toId)
      if (!target) fail('Mapped client id not found.', pendingId)
      // Sanity: the mapped client must belong to the resolved operator.
      if (target.operator_id !== opId) {
        fail(
          `Mapped client "${target.name}" is under a different operator than expected.`,
          pendingId,
        )
      }
      clResolved.set(opClientKey, target.id)
      clCanonical.set(opClientKey, target.name)
    } else {
      const { data: newCl, error } = await supabase
        .from('clients')
        .insert({ org_id: org.id, operator_id: opId, name: clName })
        .select('id, name')
        .single<{ id: string; name: string }>()
      if (error || !newCl) {
        fail(`Failed to create client "${clName}": ${error?.message}`, pendingId)
      }
      clResolved.set(opClientKey, newCl.id)
      clCanonical.set(opClientKey, newCl.name)
    }
  }

  // Step 3: projects. Key is (operator-raw, client-raw, project-raw).
  const { data: existingProjects } = await supabase
    .from('projects')
    .select('id, client_id, name')
    .eq('org_id', org.id)
    .returns<{ id: string; client_id: string; name: string }[]>()
  const prByClientAndLowerName = new Map<string, { id: string; name: string }>()
  for (const p of existingProjects ?? []) {
    prByClientAndLowerName.set(`${p.client_id}|${p.name.toLowerCase()}`, p)
  }

  const prResolved = new Map<string, string>() // "op|cl|pr" → project_id
  const prCanonical = new Map<string, string>()
  const distinctTriples = new Set<string>()
  for (const e of parsedEntries) {
    if (e.operator && e.client && e.project) {
      distinctTriples.add(`${e.operator}|${e.client}|${e.project}`)
    }
  }
  for (const tripleKey of distinctTriples) {
    const [opName, clName, prName] = tripleKey.split('|', 3)
    const opId = opResolved.get(opName)!
    const clId = clResolved.get(`${opName}|${clName}`)!
    const existing = prByClientAndLowerName.get(`${clId}|${prName.toLowerCase()}`)
    if (existing) {
      prResolved.set(tripleKey, existing.id)
      prCanonical.set(tripleKey, existing.name)
      continue
    }
    const decision = prDecisions.get(tripleKey)
    if (!decision) {
      fail(
        `No decision for unknown project "${prName}" under "${opName} / ${clName}".`,
        pendingId,
      )
    }
    if (decision.kind === 'map') {
      const target = (existingProjects ?? []).find((p) => p.id === decision.toId)
      if (!target) fail('Mapped project id not found.', pendingId)
      if (target.client_id !== clId) {
        fail(
          `Mapped project "${target.name}" is under a different client than expected.`,
          pendingId,
        )
      }
      prResolved.set(tripleKey, target.id)
      prCanonical.set(tripleKey, target.name)
    } else {
      const { data: newPr, error } = await supabase
        .from('projects')
        .insert({ org_id: org.id, client_id: clId, name: prName })
        .select('id, name')
        .single<{ id: string; name: string }>()
      if (error || !newPr) {
        fail(`Failed to create project "${prName}": ${error?.message}`, pendingId)
      }
      prResolved.set(tripleKey, newPr.id)
      prCanonical.set(tripleKey, newPr.name)
    }
  }

  // Now build the actual `time_entries` rows. For each parsed entry:
  //   - look up the resolved project_id
  //   - replace operator/client/project text columns with the canonical
  //     names (post-resolution, so "Vicotria" → "Victoria" when mapped)
  //   - apply team_member conversion + locked cost (unchanged math)
  const team = await getTeamForUser(supabase, org.id, eu.effective_user_id)
  if (!team) {
    fail("You don't have a team — can't compute conversions.", pendingId)
  }
  const { data: members } = await supabase
    .from('team_members')
    .select(
      'id, email, display_name, rate_proportion, consolidate_as, cost_rate_usd, billout_rate_usd',
    )
    .eq('team_id', team.id)
  const lookup: TeamLookup = new Map()
  for (const m of members ?? []) {
    lookup.set(m.email.toLowerCase(), {
      id: m.id,
      display_name: m.display_name,
      rate_proportion: Number(m.rate_proportion),
      consolidate_as: m.consolidate_as,
      cost_rate_usd:
        m.cost_rate_usd == null ? null : Number(m.cost_rate_usd),
      billout_rate_usd:
        m.billout_rate_usd == null ? null : Number(m.billout_rate_usd),
    })
  }

  // Insert the clockify_imports batch.
  const { data: batch, error: batchErr } = await supabase
    .from('clockify_imports')
    .insert({
      org_id: org.id,
      imported_by: eu.effective_user_id,
      filename: pending.filename,
      name: pending.batch_name,
      row_count: parsedEntries.length,
      notes: pending.notes,
      source: pending.source,
    })
    .select('id')
    .single<{ id: string }>()
  if (batchErr || !batch) {
    fail(batchErr?.message ?? 'Failed to create import batch.', pendingId)
  }

  // Cache effective rate per (team_member_id, project_id) so we don't
  // hammer the RPC once per row when the import has many entries for
  // the same person/project.
  const effRateCache = new Map<string, number | null>()
  async function effRateFor(
    teamMemberId: string | null,
    projectId: string | null,
  ): Promise<number | null> {
    if (teamMemberId == null) return null
    const k = `${teamMemberId}|${projectId ?? ''}`
    if (effRateCache.has(k)) return effRateCache.get(k) ?? null
    const r = await getEffectiveBilloutRate(supabase, teamMemberId, projectId)
    effRateCache.set(k, r)
    return r
  }

  // After the eliminate-proportion migration, cost = source × cost_rate_usd
  // (per-member) and team.base_rate_usd is vestigial. The Toggl-export
  // hours (converted_duration_seconds) is still computed below via
  // applyConversion (source × proportion) for downstream invoicing.
  const rows: Record<string, unknown>[] = []
  for (const e of parsedEntries) {
    const conv = applyConversion(e, lookup)
    const billoutCost =
      e.duration_seconds == null || conv.cost_rate_usd == null
        ? null
        : Math.round((e.duration_seconds / 3600) * conv.cost_rate_usd * 100) / 100

    // Resolve to canonical names + project_id when all three parts exist.
    let projectId: string | null = null
    let operatorOut: string | null = e.operator || null
    let clientOut: string | null = e.client
    let projectOut: string | null = e.project
    if (e.operator && e.client && e.project) {
      const tripleKey = `${e.operator}|${e.client}|${e.project}`
      projectId = prResolved.get(tripleKey) ?? null
      operatorOut = opCanonical.get(e.operator) ?? e.operator
      clientOut = clCanonical.get(`${e.operator}|${e.client}`) ?? e.client
      projectOut = prCanonical.get(tripleKey) ?? e.project
    } else {
      // Partial: best-effort canonical names where available.
      if (e.operator) operatorOut = opCanonical.get(e.operator) ?? e.operator
      if (e.operator && e.client) {
        clientOut = clCanonical.get(`${e.operator}|${e.client}`) ?? e.client
      }
    }

    // Effective billout rate (project-override-aware). Per-source-hour
    // after the eliminate-proportion migration.
    const effectiveRate = await effRateFor(conv.team_member_id, projectId)
    const rateForBillout = effectiveRate ?? conv.billout_rate_usd
    const billoutAmount = computeBilloutAmount(
      e.duration_seconds,
      rateForBillout,
    )

    rows.push({
      org_id: org.id,
      import_id: batch.id,
      project_id: projectId,
      source_user_name: e.source_user_name,
      source_user_email: e.source_user_email,
      operator: operatorOut,
      client: clientOut,
      project: projectOut,
      description: e.description,
      billable: e.billable,
      start_at: e.start_at,
      end_at: e.end_at,
      duration_seconds: e.duration_seconds,
      source_rate_usd: e.source_rate_usd,
      source_amount_usd: e.source_amount_usd,
      team_member_id: conv.team_member_id,
      converted_user: conv.converted_user,
      converted_duration_seconds: conv.converted_duration_seconds,
      billout_cost_usd: billoutCost,
      billout_amount_usd: billoutAmount,
    })
  }

  const CHUNK = 500
  for (let i = 0; i < rows.length; i += CHUNK) {
    const { error } = await supabase
      .from('time_entries')
      .insert(rows.slice(i, i + CHUNK))
    if (error) {
      // Rollback the batch on failure. Pending row stays so the user
      // can retry from the resolver.
      await supabase.from('time_entries').delete().eq('import_id', batch.id)
      await supabase.from('clockify_imports').delete().eq('id', batch.id)
      fail(`Insert failed at rows ${i + 1}–${i + CHUNK}: ${error.message}`, pendingId)
    }
  }

  // Clean up the pending row.
  await supabase.from('pending_imports').delete().eq('id', pending.id)

  revalidatePath('/import')
  revalidatePath('/projects')
  revalidatePath('/projects/manage')
  revalidatePath('/transfer')
  revalidatePath('/entries')
  redirect(`/import?import_id=${batch.id}`)
}

export async function cancelPendingImport(formData: FormData) {
  const id = String(formData.get('pending_id') ?? '')
  if (!id) redirect('/import')
  const supabase = await createClient()
  await supabase.from('pending_imports').delete().eq('id', id)
  redirect('/import')
}
