'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,
  getViewAsImportScope,
} from '@/lib/effective-user'
import { canManageImports, canTransfer } from '@/lib/permissions'
import { readFiltersFromSearchParams } from '@/lib/filters'

/**
 * Set (or clear) the income for a single project group. Owner / super_admin
 * only — both at the app layer here and at the RLS layer on `projects`.
 *
 * Form fields:
 *   - `operator`, `client`, `project`  the group key. Empty string = NULL
 *     for that column (NULL-safe key, matches the projects table's unique
 *     index).
 *   - `income_usd`  number, or empty to clear.
 *   - `_return_url`  optional; appended back so the page restores filter
 *     state after the action.
 */
export async function updateProjectIncome(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 edit project income.')
  }

  const operatorRaw = String(formData.get('operator') ?? '')
  const clientRaw = String(formData.get('client') ?? '')
  const projectRaw = String(formData.get('project') ?? '')
  const operator = operatorRaw === '' ? null : operatorRaw
  const client = clientRaw === '' ? null : clientRaw
  const project = projectRaw === '' ? null : projectRaw

  function readOptionalNumber(key: string, label: string): number | null {
    const raw = String(formData.get(key) ?? '').trim()
    if (raw === '') return null
    const n = Number(raw.replace(/[$,%]/g, ''))
    if (!Number.isFinite(n)) {
      fail(`${label} must be a number, or empty to clear.`)
    }
    return Math.round(n * 100) / 100
  }

  const incomeRaw = String(formData.get('income_usd') ?? '').trim()
  let income: number | null = null
  if (incomeRaw !== '') {
    const n = Number(incomeRaw.replace(/[$,]/g, ''))
    if (!Number.isFinite(n) || n < 0) {
      fail('Income must be a non-negative number, or empty to clear.')
    }
    income = Math.round(n * 100) / 100
  }

  // Billout adjustments. Both nullable; either or both can be set.
  // pct: signed % (negative = discount). amount: signed $.
  const billoutAdjPct = readOptionalNumber(
    'billout_adjustment_pct',
    'Adjustment %',
  )
  const billoutAdjAmount = readOptionalNumber(
    'billout_adjustment_amount',
    'Adjustment amount',
  )

  const returnUrl = (() => {
    const raw = String(formData.get('_return_url') ?? '').trim()
    return raw.startsWith('/projects') ? raw : '/projects'
  })()

  // Walk the entity chain to find the projects row matching this
  // (operator, client, project) tuple. After migration 20260524000001
  // the projects table has client_id + name (no more text columns), so
  // we resolve via operators → clients → projects.
  if (operator === null || client === null || project === null) {
    fail(
      'Cannot set income on a project group with missing operator, client, or project. Fix the entries first.',
    )
  }
  const { data: op } = await supabase
    .from('operators')
    .select('id')
    .eq('org_id', org.id)
    .ilike('name', operator)
    .maybeSingle<{ id: string }>()
  if (!op) fail(`Operator "${operator}" not found.`)
  const { data: cl } = await supabase
    .from('clients')
    .select('id')
    .eq('operator_id', op.id)
    .ilike('name', client)
    .maybeSingle<{ id: string }>()
  if (!cl) fail(`Client "${client}" not found under operator "${operator}".`)
  const { data: existing, error: lookupErr } = await supabase
    .from('projects')
    .select('id')
    .eq('client_id', cl.id)
    .ilike('name', project)
    .maybeSingle<{ id: string }>()
  if (lookupErr) fail(lookupErr.message)

  const patch = {
    income_usd: income,
    billout_adjustment_pct: billoutAdjPct,
    billout_adjustment_amount: billoutAdjAmount,
  }
  const hasAnyValue =
    income !== null || billoutAdjPct !== null || billoutAdjAmount !== null

  if (existing) {
    const { error } = await supabase
      .from('projects')
      .update(patch)
      .eq('id', existing.id)
    if (error) fail(error.message)
  } else if (hasAnyValue) {
    // Project entity doesn't exist yet (shouldn't happen post-migration
    // since the backfill created one per (op, client, project) tuple,
    // but defensively we'll create it). Insert with the resolved client_id.
    const { error } = await supabase.from('projects').insert({
      org_id: org.id,
      client_id: cl.id,
      name: project,
      ...patch,
    })
    if (error) fail(error.message)
  }

  revalidatePath('/projects')
  redirect(returnUrl)
}

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

/**
 * Bulk-update `operator` / `client` / `project` on every `time_entries`
 * row that falls into any of the selected project groups. Operates on
 * the same tuple grouping the /projects page shows.
 *
 * Form fields:
 *   - `project_key`  one per selected group, encoded `operator|client|project`
 *                    with `''` standing in for NULL columns
 *   - `bulk_operator`, `bulk_client`, `bulk_project`  — non-empty fields are
 *                    applied. Empty inputs are skipped.
 *   - `filter_*`     — same filter snapshot as /entries / /projects, used so
 *                    we can constrain the update to the active filter (and
 *                    so transferred-row protection for non-owners matches
 *                    what the page is showing).
 */
export async function bulkUpdateProjectFields(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 (!canManageImports(eu)) fail('You do not have permission to bulk edit.')

  const keys = formData
    .getAll('project_key')
    .map(String)
    .filter(Boolean)
  if (keys.length === 0) fail('No project groups selected.')

  const newOperator = String(formData.get('bulk_operator') ?? '').trim()
  const newClient = String(formData.get('bulk_client') ?? '').trim()
  const newProject = String(formData.get('bulk_project') ?? '').trim()

  const updatePayload: Record<string, string> = {}
  if (newOperator) updatePayload.operator = newOperator
  if (newClient) updatePayload.client = newClient
  if (newProject) updatePayload.project = newProject

  if (Object.keys(updatePayload).length === 0) {
    fail('No fields provided for bulk edit.')
  }

  // Same filter snapshot as the /entries pattern — used here only to
  // honor the page-level "restrict to untransferred" for non-owners.
  // We don't intersect with the filter on a per-row basis: the user
  // explicitly selected these project groups, so the change applies to
  // every entry in them (subject to transferred-row protection).
  const filterFormData: Record<string, string> = {}
  for (const [k, v] of formData.entries()) {
    if (k.startsWith('filter_') && typeof v === 'string') {
      filterFormData[k.replace(/^filter_/, '')] = v
    }
  }
  // We don't need date/etc here; we keep them only to mirror /entries'
  // future need. The actual scope is the project tuples themselves.
  void readFiltersFromSearchParams(filterFormData)

  const restrictToUntransferred = !canTransfer(eu)

  // View-as scope: outside view-as this is null and inner predicate is a no-op.
  const viewAsScope = await getViewAsImportScope(supabase, org.id, eu)

  // Parse each "operator|client|project" key. NULL fields are encoded
  // as empty strings; we map back to is(null) below.
  type Tuple = { operator: string | null; client: string | null; project: string | null }
  const tuples: Tuple[] = keys.map((k) => {
    const parts = k.split('|')
    return {
      operator: parts[0] === '' ? null : parts[0],
      client: parts[1] === '' ? null : parts[1],
      project: parts[2] === '' ? null : parts[2],
    }
  })

  let totalUpdated = 0
  for (const t of tuples) {
    let q = supabase
      .from('time_entries')
      .update(updatePayload)
      .eq('org_id', org.id)
    // NULL-safe column matching via is/eq.
    q = t.operator === null ? q.is('operator', null) : q.eq('operator', t.operator)
    q = t.client === null ? q.is('client', null) : q.eq('client', t.client)
    q = t.project === null ? q.is('project', null) : q.eq('project', t.project)
    if (restrictToUntransferred) q = q.is('transferred_at', null)
    if (viewAsScope) q = q.in('import_id', viewAsScope)
    const { data, error } = await q.select('id')
    if (error) {
      console.error('[bulkUpdateProjectFields] update failed', error, t)
      fail(error.message)
    }
    totalUpdated += data?.length ?? 0
  }

  revalidatePath('/projects')
  revalidatePath('/entries')
  revalidatePath('/transfer')
  revalidatePath('/')
  redirect(`/projects?bulk_edited=${totalUpdated}`)
}

/**
 * Re-stamp `billout_amount_usd` on every entry that points at `projectId`
 * (optionally narrowed to a single team_member). Single SQL UPDATE via
 * the `restamp_billout_for_project` RPC + the `effective_billout_rate`
 * function. ~1 round-trip regardless of row count.
 */
async function restampEntriesForProject(
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  supabase: any,
  orgId: string,
  projectId: string,
  teamMemberId: string | null,
): Promise<number> {
  const { data, error } = await supabase.rpc('restamp_billout_for_project', {
    p_org_id: orgId,
    p_project_id: projectId,
    p_team_member_id: teamMemberId,
  })
  if (error) throw error
  return data == null ? 0 : Number(data)
}

/**
 * Insert or update a rate override on a project. When team_member_id is
 * empty, the rule applies to all entries on the project (the default).
 * After the upsert we re-stamp affected entries.
 *
 * Form fields:
 *   - project_id (required)
 *   - team_member_id (optional — blank/empty = the "all" default rule)
 *   - override_rate_usd (required, >= 0)
 *   - _return_url (optional)
 */
export async function upsertProjectRateOverride(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 edit rate overrides.')
  }

  const projectId = String(formData.get('project_id') ?? '').trim()
  if (!projectId) fail('Missing project id.')

  const teamMemberRaw = String(formData.get('team_member_id') ?? '').trim()
  const teamMemberId = teamMemberRaw === '' ? null : teamMemberRaw

  // The form has a "kind" select (rate | pct) and one value input.
  // Exactly one of override_rate_usd / override_pct is set per row
  // (enforced by a DB CHECK constraint).
  const kind = String(formData.get('override_kind') ?? 'rate').toLowerCase()
  const valueRaw = String(formData.get('override_value') ?? '').trim()
  if (valueRaw === '') {
    fail('Override value is required. To remove a rule, use Delete instead.')
  }
  const numeric = Number(valueRaw.replace(/[$,%]/g, ''))
  if (!Number.isFinite(numeric)) {
    fail('Override value must be a number.')
  }

  let overrideRate: number | null = null
  let overridePct: number | null = null
  if (kind === 'pct') {
    // Signed percentage: -100 ≤ pct (negatives = discount).
    if (numeric < -100) {
      fail('Percentage discount cannot be less than -100% (which is free).')
    }
    overridePct = Math.round(numeric * 10000) / 10000
  } else {
    if (numeric < 0) {
      fail('Absolute rate must be non-negative.')
    }
    overrideRate = Math.round(numeric * 100) / 100
  }

  // Upsert. PostgREST doesn't have natural multi-column upsert with
  // coalesce-style uniqueness, so do find-or-insert manually.
  let existingId: string | null = null
  {
    let q = supabase
      .from('project_rate_overrides')
      .select('id')
      .eq('project_id', projectId)
    q = teamMemberId === null
      ? q.is('team_member_id', null)
      : q.eq('team_member_id', teamMemberId)
    const { data } = await q.maybeSingle<{ id: string }>()
    existingId = data?.id ?? null
  }
  if (existingId) {
    const { error } = await supabase
      .from('project_rate_overrides')
      .update({
        override_rate_usd: overrideRate,
        override_pct: overridePct,
      })
      .eq('id', existingId)
    if (error) fail(error.message)
  } else {
    const { error } = await supabase.from('project_rate_overrides').insert({
      org_id: org.id,
      project_id: projectId,
      team_member_id: teamMemberId,
      override_rate_usd: overrideRate,
      override_pct: overridePct,
    })
    if (error) fail(error.message)
  }

  // Re-stamp affected entries.
  let restamped = 0
  try {
    // Pass null team_member when the rule is project-wide so the
    // re-stamp covers everyone — even users whose entries fall through
    // to the new project default. (If the rule is user-specific, we
    // only need to re-stamp that user's rows.)
    restamped = await restampEntriesForProject(
      supabase,
      org.id,
      projectId,
      teamMemberId,
    )
  } catch (err) {
    console.error('[upsertProjectRateOverride] re-stamp failed', err)
    fail('Saved the override but failed to re-stamp some entries: ' + String(err))
  }

  revalidatePath('/projects')
  revalidatePath('/entries')
  revalidatePath('/')
  const ret = String(formData.get('_return_url') ?? '').trim() || '/projects'
  const url = new URL(ret, 'http://x')
  url.searchParams.set(
    'info',
    `Saved override · re-stamped ${restamped} ${restamped === 1 ? 'entry' : 'entries'}.`,
  )
  redirect(url.pathname + (url.search || ''))
}

export async function deleteProjectRateOverride(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 delete rate overrides.')
  }

  const overrideId = String(formData.get('id') ?? '').trim()
  if (!overrideId) fail('Missing override id.')

  // Load the row first so we know the project + team_member scope to re-stamp.
  const { data: row } = await supabase
    .from('project_rate_overrides')
    .select('project_id, team_member_id')
    .eq('id', overrideId)
    .maybeSingle<{ project_id: string; team_member_id: string | null }>()
  if (!row) fail('Override not found.')

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

  let restamped = 0
  try {
    restamped = await restampEntriesForProject(
      supabase,
      org.id,
      row.project_id,
      row.team_member_id,
    )
  } catch (err) {
    console.error('[deleteProjectRateOverride] re-stamp failed', err)
    fail('Deleted the override but failed to re-stamp some entries: ' + String(err))
  }

  revalidatePath('/projects')
  revalidatePath('/entries')
  revalidatePath('/')
  const ret = String(formData.get('_return_url') ?? '').trim() || '/projects'
  const url = new URL(ret, 'http://x')
  url.searchParams.set(
    'info',
    `Deleted override · re-stamped ${restamped} ${restamped === 1 ? 'entry' : 'entries'}.`,
  )
  redirect(url.pathname + (url.search || ''))
}
