'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'

function fail(msg: string): never {
  redirect(`/projects/manage?error=${encodeURIComponent(msg)}`)
}

function ok(msg: string): never {
  redirect(`/projects/manage?info=${encodeURIComponent(msg)}`)
}

function readName(formData: FormData, fieldKey: string): string {
  const v = String(formData.get(fieldKey) ?? '').trim()
  if (!v) fail('Name is required.')
  return v
}

/**
 * Rename an operator. Updates the operators row AND propagates the new
 * name to every `time_entries.operator` cell that references it (via
 * the projects → clients → operator chain).
 *
 * The denormalized text cache only matters for filters / RPCs that
 * still read the text columns. Once everything migrates to project_id
 * joins, this propagation step becomes optional. For now it keeps the
 * existing UI working unchanged.
 */
export async function renameOperator(formData: FormData) {
  const id = String(formData.get('id') ?? '')
  if (!id) fail('Missing operator id.')
  const newName = readName(formData, 'name')

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

  const eu = await getEffectiveUser(supabase, org.id)
  if (!canManageImports(eu)) fail('You do not have permission to rename entities.')

  // Load existing row to grab the old name for the propagation step.
  const { data: existing } = await supabase
    .from('operators')
    .select('id, name, org_id')
    .eq('id', id)
    .maybeSingle<{ id: string; name: string; org_id: string }>()
  if (!existing) fail('Operator not found.')
  if (existing.name === newName) ok('No change — name was already that.')

  // Conflict pre-check (case-insensitive). The DB will reject anyway via
  // the unique index, but a nice error is friendlier.
  const { data: dupe } = await supabase
    .from('operators')
    .select('id')
    .eq('org_id', existing.org_id)
    .ilike('name', newName)
    .neq('id', id)
    .maybeSingle<{ id: string }>()
  if (dupe) fail(`Another operator already has the name "${newName}".`)

  // 1. Rename the entity row.
  const { error: renameErr } = await supabase
    .from('operators')
    .update({ name: newName })
    .eq('id', id)
  if (renameErr) fail(renameErr.message)

  // 2. Propagate to the denormalized cache on time_entries. We walk
  //    operator → clients → projects → entries via project_id, then
  //    UPDATE the operator text column on those rows. This is the
  //    correct path even if there's case-mismatch in the cache, and
  //    it's the only way to rename safely when the same client name
  //    exists under multiple operators.
  const { data: clients } = await supabase
    .from('clients')
    .select('id')
    .eq('operator_id', id)
    .returns<{ id: string }[]>()
  const clientIds = (clients ?? []).map((c) => c.id)
  let entriesUpdated = 0
  if (clientIds.length > 0) {
    const { data: projects } = await supabase
      .from('projects')
      .select('id')
      .in('client_id', clientIds)
      .returns<{ id: string }[]>()
    const projectIds = (projects ?? []).map((p) => p.id)
    if (projectIds.length > 0) {
      const { data: updated, error: updErr } = await supabase
        .from('time_entries')
        .update({ operator: newName })
        .in('project_id', projectIds)
        .select('id')
      if (updErr) fail(updErr.message)
      entriesUpdated = updated?.length ?? 0
    }
  }

  revalidatePath('/projects/manage')
  revalidatePath('/projects')
  revalidatePath('/entries')
  revalidatePath('/')
  ok(`Renamed to "${newName}". Updated ${entriesUpdated} entries.`)
}

export async function renameClient(formData: FormData) {
  const id = String(formData.get('id') ?? '')
  if (!id) fail('Missing client id.')
  const newName = readName(formData, 'name')

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

  const eu = await getEffectiveUser(supabase, org.id)
  if (!canManageImports(eu)) fail('You do not have permission to rename entities.')

  const { data: existing } = await supabase
    .from('clients')
    .select('id, name, operator_id, org_id')
    .eq('id', id)
    .maybeSingle<{
      id: string
      name: string
      operator_id: string
      org_id: string
    }>()
  if (!existing) fail('Client not found.')
  if (existing.name === newName) ok('No change — name was already that.')

  // Conflict pre-check (unique per operator).
  const { data: dupe } = await supabase
    .from('clients')
    .select('id')
    .eq('operator_id', existing.operator_id)
    .ilike('name', newName)
    .neq('id', id)
    .maybeSingle<{ id: string }>()
  if (dupe) {
    fail(
      `Another client under this operator already has the name "${newName}".`,
    )
  }

  const { error: renameErr } = await supabase
    .from('clients')
    .update({ name: newName })
    .eq('id', id)
  if (renameErr) fail(renameErr.message)

  // Propagate to time_entries: find projects for this client, then update entries.
  const { data: projects } = await supabase
    .from('projects')
    .select('id')
    .eq('client_id', id)
    .returns<{ id: string }[]>()
  const projectIds = (projects ?? []).map((p) => p.id)
  let entriesUpdated = 0
  if (projectIds.length > 0) {
    const { data: updated, error: updErr } = await supabase
      .from('time_entries')
      .update({ client: newName })
      .in('project_id', projectIds)
      .select('id')
    if (updErr) fail(updErr.message)
    entriesUpdated = updated?.length ?? 0
  }

  revalidatePath('/projects/manage')
  revalidatePath('/projects')
  revalidatePath('/entries')
  revalidatePath('/')
  ok(`Renamed to "${newName}". Updated ${entriesUpdated} entries.`)
}

export async function renameProject(formData: FormData) {
  const id = String(formData.get('id') ?? '')
  if (!id) fail('Missing project id.')
  const newName = readName(formData, 'name')

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

  const eu = await getEffectiveUser(supabase, org.id)
  if (!canManageImports(eu)) fail('You do not have permission to rename entities.')

  const { data: existing } = await supabase
    .from('projects')
    .select('id, name, client_id')
    .eq('id', id)
    .maybeSingle<{ id: string; name: string; client_id: string }>()
  if (!existing) fail('Project not found.')
  if (existing.name === newName) ok('No change — name was already that.')

  // Conflict pre-check (unique per client).
  const { data: dupe } = await supabase
    .from('projects')
    .select('id')
    .eq('client_id', existing.client_id)
    .ilike('name', newName)
    .neq('id', id)
    .maybeSingle<{ id: string }>()
  if (dupe) {
    fail(
      `Another project under this client already has the name "${newName}".`,
    )
  }

  const { error: renameErr } = await supabase
    .from('projects')
    .update({ name: newName })
    .eq('id', id)
  if (renameErr) fail(renameErr.message)

  const { data: updated, error: updErr } = await supabase
    .from('time_entries')
    .update({ project: newName })
    .eq('project_id', id)
    .select('id')
  if (updErr) fail(updErr.message)
  const entriesUpdated = updated?.length ?? 0

  revalidatePath('/projects/manage')
  revalidatePath('/projects')
  revalidatePath('/entries')
  revalidatePath('/')
  ok(`Renamed to "${newName}". Updated ${entriesUpdated} entries.`)
}

/**
 * Create a project manually under an existing (operator, client) pair.
 * The submitter picks an existing client via the combobox; the project
 * name is free text.
 *
 * Fails if a project with this name already exists under the same
 * client. To create a new operator or client, use the entries edit
 * form's cascading combobox (which auto-creates them) or the import
 * resolver — neither path is wired here yet.
 */
export async function createProject(formData: FormData) {
  const clientId = String(formData.get('client_id') ?? '').trim()
  if (!clientId) fail('Pick a client.')
  const newName = readName(formData, 'name')

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

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

  const { data: client } = await supabase
    .from('clients')
    .select('id, name')
    .eq('id', clientId)
    .eq('org_id', org.id)
    .maybeSingle<{ id: string; name: string }>()
  if (!client) fail('Client not found in this organization.')

  const { data: dupe } = await supabase
    .from('projects')
    .select('id')
    .eq('client_id', clientId)
    .ilike('name', newName)
    .maybeSingle<{ id: string }>()
  if (dupe) {
    fail(`A project named "${newName}" already exists under that client.`)
  }

  const { error } = await supabase
    .from('projects')
    .insert({ org_id: org.id, client_id: clientId, name: newName })
  if (error) fail(error.message)

  revalidatePath('/projects/manage')
  revalidatePath('/projects')
  ok(`Created project "${newName}" under ${client.name}.`)
}

/**
 * Delete a project, reassigning every time_entry + cc_expense_line
 * that pointed at it to a target project first. Treat it as
 * "merge into target, then delete source".
 *
 * Rewrites on the entries:
 *   - project_id   → target.id
 *   - operator     → target.client.operator.name
 *   - client       → target.client.name
 *   - project      → target.name
 *
 * Rewrites on adjacent rows:
 *   - cc_expense_lines.project_id → target.id (preserves billing
 *     pass-through history when the source project goes away).
 *   - project_rate_overrides on the source project are deleted —
 *     target's overrides apply going forward; merging override
 *     semantics is out of scope.
 *
 * The source's income_usd / billout_adjustment_* are NOT migrated
 * to the target — they're lost. The owner is choosing to merge,
 * which implies the target's values take precedence going forward.
 */
export async function deleteProject(formData: FormData) {
  const sourceId = String(formData.get('id') ?? '').trim()
  if (!sourceId) fail('Missing source project id.')
  const targetId =
    String(formData.get('target_project_id') ?? '').trim() || null
  if (targetId && sourceId === targetId) {
    fail("Source and target are the same project — can't merge into self.")
  }

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

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

  const { data: source } = await supabase
    .from('projects')
    .select('id, name')
    .eq('id', sourceId)
    .eq('org_id', org.id)
    .maybeSingle<{ id: string; name: string }>()
  if (!source) fail('Source project not found.')

  // Need a target only if there are entries or CC expense lines
  // pointing at the source. Otherwise straight delete.
  const [{ count: entriesCount }, { count: ccCount }] = await Promise.all([
    supabase
      .from('time_entries')
      .select('id', { count: 'exact', head: true })
      .eq('org_id', org.id)
      .eq('project_id', source.id),
    supabase
      .from('cc_expense_lines')
      .select('id', { count: 'exact', head: true })
      .eq('project_id', source.id),
  ])
  const hasChildren = (entriesCount ?? 0) > 0 || (ccCount ?? 0) > 0

  let target:
    | {
        id: string
        name: string
        client: {
          name: string
          operator: { name: string } | null
        } | null
      }
    | null = null
  if (hasChildren) {
    if (!targetId) fail('Pick a project to reassign entries to.')
    const { data } = await supabase
      .from('projects')
      .select('id, name, client:clients(name, operator:operators(name))')
      .eq('id', targetId)
      .eq('org_id', org.id)
      .maybeSingle<{
        id: string
        name: string
        client: {
          name: string
          operator: { name: string } | null
        } | null
      }>()
    if (!data) fail('Target project not found.')
    if (!data.client?.operator?.name) {
      fail('Target project is missing its client / operator chain.')
    }
    target = data
  }

  let entriesMoved = 0
  let expensesMoved = 0
  if (target) {
    // 1. Reassign time_entries.
    const { data: movedEntries, error: teErr } = await supabase
      .from('time_entries')
      .update({
        project_id: target.id,
        operator: target.client!.operator!.name,
        client: target.client!.name,
        project: target.name,
      })
      .eq('org_id', org.id)
      .eq('project_id', source.id)
      .select('id')
    if (teErr) fail(teErr.message)
    entriesMoved = movedEntries?.length ?? 0

    // 2. Reassign cc_expense_lines that pointed at the source.
    const { data: movedExpenses, error: ccErr } = await supabase
      .from('cc_expense_lines')
      .update({ project_id: target.id })
      .eq('project_id', source.id)
      .select('id')
    if (ccErr) fail(ccErr.message)
    expensesMoved = movedExpenses?.length ?? 0
  }

  // 3. Drop project_rate_overrides for the source.
  const { error: ovErr } = await supabase
    .from('project_rate_overrides')
    .delete()
    .eq('project_id', source.id)
  if (ovErr) fail(ovErr.message)

  // 4. Finally delete the source project.
  const { error: delErr } = await supabase
    .from('projects')
    .delete()
    .eq('id', source.id)
    .eq('org_id', org.id)
  if (delErr) fail(delErr.message)

  revalidatePath('/projects/manage')
  revalidatePath('/projects')
  revalidatePath('/entries')
  revalidatePath('/invoices')
  revalidatePath('/tools/cc-expenses')
  revalidatePath('/')
  if (target) {
    ok(
      `Deleted "${source.name}". Reassigned ${entriesMoved} entries` +
        (expensesMoved ? ` + ${expensesMoved} CC expense lines` : '') +
        ` to "${target.client!.name} : ${target.name}".`,
    )
  } else {
    ok(`Deleted empty project "${source.name}".`)
  }
}

/**
 * Delete a client. If it has projects, move them under a target
 * client first (the projects keep their identities but change parent).
 *
 * Rewrites:
 *   - projects.client_id → target.id (for every project under source)
 *   - time_entries.client → target.name AND time_entries.operator →
 *     target.operator.name for every entry under those projects
 *     (handles cross-operator moves correctly).
 *   - invoices.client_id → target.id where it pointed at source.
 *
 * Empty-source case: target is optional; source is deleted directly.
 */
export async function deleteClient(formData: FormData) {
  const sourceId = String(formData.get('id') ?? '').trim()
  if (!sourceId) fail('Missing source client id.')
  const targetId =
    String(formData.get('target_client_id') ?? '').trim() || null
  if (targetId && sourceId === targetId) {
    fail("Source and target are the same client — can't merge into self.")
  }

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

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

  const { data: source } = await supabase
    .from('clients')
    .select('id, name')
    .eq('id', sourceId)
    .eq('org_id', org.id)
    .maybeSingle<{ id: string; name: string }>()
  if (!source) fail('Source client not found.')

  // Find projects under the source client.
  const { data: sourceProjects } = await supabase
    .from('projects')
    .select('id')
    .eq('client_id', source.id)
    .eq('org_id', org.id)
    .returns<{ id: string }[]>()
  const sourceProjectIds = (sourceProjects ?? []).map((p) => p.id)
  const hasChildren = sourceProjectIds.length > 0

  let target:
    | { id: string; name: string; operator: { name: string } | null }
    | null = null
  if (hasChildren) {
    if (!targetId) fail('Pick a client to reassign projects to.')
    const { data } = await supabase
      .from('clients')
      .select('id, name, operator:operators(name)')
      .eq('id', targetId)
      .eq('org_id', org.id)
      .maybeSingle<{
        id: string
        name: string
        operator: { name: string } | null
      }>()
    if (!data) fail('Target client not found.')
    if (!data.operator?.name) {
      fail('Target client is missing its operator.')
    }
    target = data
  }

  let projectsMoved = 0
  let entriesUpdated = 0
  if (target) {
    // 1. Re-parent projects.
    const { data: moved, error: pErr } = await supabase
      .from('projects')
      .update({ client_id: target.id })
      .in('id', sourceProjectIds)
      .select('id')
    if (pErr) fail(pErr.message)
    projectsMoved = moved?.length ?? 0

    // 2. Update time_entries text cache (client + operator) for
    //    every entry under the moved projects. Operator may change
    //    if the target client is under a different operator.
    const { data: te, error: teErr } = await supabase
      .from('time_entries')
      .update({
        client: target.name,
        operator: target.operator!.name,
      })
      .eq('org_id', org.id)
      .in('project_id', sourceProjectIds)
      .select('id')
    if (teErr) fail(teErr.message)
    entriesUpdated = te?.length ?? 0

    // 3. Reassign invoices.client_id.
    const { error: invErr } = await supabase
      .from('invoices')
      .update({ client_id: target.id })
      .eq('client_id', source.id)
    if (invErr) fail(invErr.message)
  }

  // 4. Delete source.
  const { error: delErr } = await supabase
    .from('clients')
    .delete()
    .eq('id', source.id)
    .eq('org_id', org.id)
  if (delErr) fail(delErr.message)

  revalidatePath('/projects/manage')
  revalidatePath('/projects')
  revalidatePath('/entries')
  revalidatePath('/invoices')
  revalidatePath('/')
  if (target) {
    ok(
      `Deleted "${source.name}". Re-parented ${projectsMoved} projects ` +
        `(${entriesUpdated} entries refreshed) to "${target.name}".`,
    )
  } else {
    ok(`Deleted empty client "${source.name}".`)
  }
}

/**
 * Delete an operator. If it has clients, move them under a target
 * operator first.
 *
 * Rewrites:
 *   - clients.operator_id → target.id (for every client under source)
 *   - time_entries.operator → target.name for every entry under any
 *     project of those moved clients.
 *   - invoices.operator_id → target.id where it pointed at source.
 *
 * Empty-source case: target is optional; source is deleted directly.
 */
export async function deleteOperator(formData: FormData) {
  const sourceId = String(formData.get('id') ?? '').trim()
  if (!sourceId) fail('Missing source operator id.')
  const targetId =
    String(formData.get('target_operator_id') ?? '').trim() || null
  if (targetId && sourceId === targetId) {
    fail("Source and target are the same operator — can't merge into self.")
  }

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

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

  const { data: source } = await supabase
    .from('operators')
    .select('id, name')
    .eq('id', sourceId)
    .eq('org_id', org.id)
    .maybeSingle<{ id: string; name: string }>()
  if (!source) fail('Source operator not found.')

  // Find clients under the source operator + projects under those clients.
  const { data: sourceClients } = await supabase
    .from('clients')
    .select('id')
    .eq('operator_id', source.id)
    .eq('org_id', org.id)
    .returns<{ id: string }[]>()
  const sourceClientIds = (sourceClients ?? []).map((c) => c.id)

  let sourceProjectIds: string[] = []
  if (sourceClientIds.length > 0) {
    const { data: sourceProjects } = await supabase
      .from('projects')
      .select('id')
      .in('client_id', sourceClientIds)
      .returns<{ id: string }[]>()
    sourceProjectIds = (sourceProjects ?? []).map((p) => p.id)
  }
  const hasChildren = sourceClientIds.length > 0

  let target: { id: string; name: string } | null = null
  if (hasChildren) {
    if (!targetId) fail('Pick an operator to reassign clients to.')
    const { data } = await supabase
      .from('operators')
      .select('id, name')
      .eq('id', targetId)
      .eq('org_id', org.id)
      .maybeSingle<{ id: string; name: string }>()
    if (!data) fail('Target operator not found.')
    target = data
  }

  let clientsMoved = 0
  let entriesUpdated = 0
  if (target) {
    // 1. Re-parent clients.
    const { data: moved, error: cErr } = await supabase
      .from('clients')
      .update({ operator_id: target.id })
      .in('id', sourceClientIds)
      .select('id')
    if (cErr) fail(cErr.message)
    clientsMoved = moved?.length ?? 0

    // 2. Update time_entries.operator text cache for any entry whose
    //    project is under the just-moved clients.
    if (sourceProjectIds.length > 0) {
      const { data: te, error: teErr } = await supabase
        .from('time_entries')
        .update({ operator: target.name })
        .eq('org_id', org.id)
        .in('project_id', sourceProjectIds)
        .select('id')
      if (teErr) fail(teErr.message)
      entriesUpdated = te?.length ?? 0
    }

    // 3. Reassign invoices.operator_id.
    const { error: invErr } = await supabase
      .from('invoices')
      .update({ operator_id: target.id })
      .eq('operator_id', source.id)
    if (invErr) fail(invErr.message)
  }

  // 4. Delete source.
  const { error: delErr } = await supabase
    .from('operators')
    .delete()
    .eq('id', source.id)
    .eq('org_id', org.id)
  if (delErr) fail(delErr.message)

  revalidatePath('/projects/manage')
  revalidatePath('/projects')
  revalidatePath('/entries')
  revalidatePath('/invoices')
  revalidatePath('/')
  if (target) {
    ok(
      `Deleted "${source.name}". Re-parented ${clientsMoved} clients ` +
        `(${entriesUpdated} entries refreshed) to "${target.name}".`,
    )
  } else {
    ok(`Deleted empty operator "${source.name}".`)
  }
}
