import Link from 'next/link'
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 { FilterBar, type FilterOptions } from '@/components/filter-bar'
import { readFiltersFromSearchParams } from '@/lib/filters'
import { EntriesTable, type Entry, type FilterSnapshot } from './entries-table'
import { EntriesSummary } from './entries-summary'
import { listVisibleTeamMembers } from '@/lib/teams'

const PAGE_SIZE = 100

const SORT_COLUMNS: Record<string, string> = {
  date: 'start_at',
  user: 'converted_user',
  source_email: 'source_user_email',
  operator: 'operator',
  client: 'client',
  project: 'project',
  description: 'description',
  source_hrs: 'duration_seconds',
  billout_hrs: 'converted_duration_seconds',
  cost: 'billout_cost_usd',
  billout_amount: 'billout_amount_usd',
  batch: 'import_id',
  status: 'transferred_at',
}

// Columns that must be non-NULL for an entry to be eligible for invoice
// application. Drops the Toggl-export-only converted_* columns now that
// invoices are the primary destination. Mirrors the SQL predicate in
// entries_filter_totals + project_summary.
const ELIGIBILITY_COLUMNS = [
  'team_member_id',
  'operator',
  'client',
  'project',
  'description',
  'end_at',
  'source_user_email',
] as const

type TotalsRow = {
  row_count: number
  source_seconds: number
  billout_seconds: number
  cost_usd: number | string
  billout_amount_usd: number | string
}

type OptionsRow = {
  operators: string[] | null
  clients: string[] | null
  projects: string[] | null
  users: string[] | null
  source_emails: string[] | null
}

export default async function EntriesPage({
  searchParams,
}: {
  searchParams: Promise<Record<string, string | string[] | undefined>>
}) {
  const sp = await searchParams
  const error = typeof sp.error === 'string' ? sp.error : null
  const info = typeof sp.info === 'string' ? sp.info : null
  const editId = typeof sp.edit === 'string' ? sp.edit : null
  const showNewEntry = sp.new === '1'
  const markedBatch = typeof sp.marked === 'string' ? sp.marked : null
  const unmarked = typeof sp.unmarked === 'string'
  const opCount = typeof sp.count === 'string' ? Number(sp.count) : null
  const bulkEdited =
    typeof sp.bulk_edited === 'string' ? Number(sp.bulk_edited) : null
  const recalculated =
    typeof sp.recalculated === 'string' ? Number(sp.recalculated) : null
  const page = Math.max(1, Number(sp.page) || 1)

  const sortKey = typeof sp.sort === 'string' ? sp.sort : 'date'
  const sortDir = (typeof sp.dir === 'string' ? sp.dir : 'desc') as 'asc' | 'desc'
  const sortColumn = SORT_COLUMNS[sortKey] ?? 'start_at'

  const supabase = await createClient()
  const org = await getAppOrg(supabase)

  if (!org) {
    return (
      <main>
        <div className="page-head">
          <h1>Entries</h1>
        </div>
        <p className="error">Bowden Works organization not found.</p>
      </main>
    )
  }

  const eu = await getEffectiveUser(supabase, org.id)
  const canEdit = canManageImports(eu)
  const userCanTransfer = canTransfer(eu)
  // View-as importer scope: when viewing-as a non-owner, restrict every
  // data-access path to that user's batches at the app layer. RLS only
  // enforces this for the real JWT user.
  const viewAsScope = await getViewAsImportScope(supabase, org.id, eu)

  const {
    startBound,
    endBound,
    status,
    batch,
    q,
    project,
    user,
    source_email,
    client,
    operator,
    imported_by,
  } = readFiltersFromSearchParams(sp, {
    // /entries defaults: all dates, status=pending
    date: 'all',
    status: 'pending',
  })

  // Team-member options for the edit-row + new-entry-row dropdown.
  // RLS gates visibility (view-as restricts to the effective user's team).
  const teamMembers = await listVisibleTeamMembers(supabase, org.id)

  // Project options for the bulk-edit picker. Format: each option
  // value is the project_id; label is "{client} : {project}".
  const { data: projectOptionsRaw } = await supabase
    .from('projects')
    .select('id, name, client:clients(name, operator:operators(name))')
    .eq('org_id', org.id)
    .order('name')
    .returns<
      {
        id: string
        name: string
        client: {
          name: string
          operator: { name: string } | null
        } | null
      }[]
    >()
  const projectOptions = (projectOptionsRaw ?? [])
    .filter((p) => p.client && p.client.operator)
    .map((p) => ({
      id: p.id,
      label: `${p.client!.name} : ${p.name}`,
    }))
    .sort((a, b) => a.label.localeCompare(b.label))

  // Open + sent invoices populate the "Apply to invoice" picker in the
  // bulk-actions bar. Paid invoices are excluded by default (apply
  // usually doesn't make sense on a paid invoice, but if it's needed
  // the user can edit the invoice's status back to sent / open first).
  const { data: invoiceOptionsRaw } = await supabase
    .from('invoices')
    .select('id, name, status')
    .eq('org_id', org.id)
    .in('status', ['open', 'sent'])
    .order('created_at', { ascending: false })
    .returns<{ id: string; name: string; status: string }[]>()
  const invoiceOptions = invoiceOptionsRaw ?? []

  // Entity name lists + parent-chain lookups for the per-row edit form.
  // The form uses datalists for autocomplete AND cascades: changing the
  // project/client field to a known-unique value auto-fills the parent
  // fields. Loading all three tables is cheap (<100 rows each).
  const [{ data: operatorRows }, { data: clientRows }, { data: projectRows }] =
    await Promise.all([
      supabase
        .from('operators')
        .select('name')
        .eq('org_id', org.id)
        .order('name')
        .returns<{ name: string }[]>(),
      supabase
        .from('clients')
        .select('name, operator:operators(name)')
        .eq('org_id', org.id)
        .order('name')
        .returns<{ name: string; operator: { name: string } | null }[]>(),
      supabase
        .from('projects')
        .select(
          'name, client:clients(name, operator:operators(name))',
        )
        .eq('org_id', org.id)
        .order('name')
        .returns<
          {
            name: string
            client: {
              name: string
              operator: { name: string } | null
            } | null
          }[]
        >(),
    ])
  const operatorNames = Array.from(
    new Set((operatorRows ?? []).map((r) => r.name)),
  )
  const clientNames = Array.from(
    new Set((clientRows ?? []).map((r) => r.name)),
  )
  const projectNames = Array.from(
    new Set((projectRows ?? []).map((r) => r.name)),
  )

  // Lowercase-keyed lookup maps for the cascading-update logic in EditRow.
  // A given name can have multiple parents (e.g. "Promobix" used to exist
  // under two operators); we only auto-fill when there's exactly ONE.
  const clientNameToOperators: Record<string, string[]> = {}
  for (const c of clientRows ?? []) {
    const op = c.operator?.name
    if (!op) continue
    const key = c.name.toLowerCase()
    if (!clientNameToOperators[key]) clientNameToOperators[key] = []
    if (!clientNameToOperators[key].includes(op))
      clientNameToOperators[key].push(op)
  }
  const projectNameToParents: Record<
    string,
    { operator: string; client: string }[]
  > = {}
  for (const p of projectRows ?? []) {
    const op = p.client?.operator?.name
    const cl = p.client?.name
    if (!op || !cl) continue
    const key = p.name.toLowerCase()
    if (!projectNameToParents[key]) projectNameToParents[key] = []
    const exists = projectNameToParents[key].some(
      (x) => x.operator === op && x.client === cl,
    )
    if (!exists) projectNameToParents[key].push({ operator: op, client: cl })
  }

  // Inverse "parent → children" maps. Used to STRICTLY narrow the
  // datalists in EditRow / NewEntryRow once a parent is picked.
  // Without these, picking a client with a duplicate-named project
  // (e.g. "10 Hour Support Block" exists under many clients) would
  // surface ALL of them in the dropdown — user picks one not
  // belonging to their client → server creates a new project under
  // their client, silently duplicating.
  const clientsByOperator: Record<string, string[]> = {}
  for (const c of clientRows ?? []) {
    const op = c.operator?.name
    if (!op) continue
    const key = op.toLowerCase()
    if (!clientsByOperator[key]) clientsByOperator[key] = []
    if (!clientsByOperator[key].includes(c.name))
      clientsByOperator[key].push(c.name)
  }
  // Project list keyed by lowercase `${operator}|${client}` — client
  // names alone aren't unique across operators, so the (op, cl) pair
  // is the right grain.
  const projectsByClientPair: Record<string, string[]> = {}
  for (const p of projectRows ?? []) {
    const op = p.client?.operator?.name
    const cl = p.client?.name
    if (!op || !cl) continue
    const key = `${op.toLowerCase()}|${cl.toLowerCase()}`
    if (!projectsByClientPair[key]) projectsByClientPair[key] = []
    if (!projectsByClientPair[key].includes(p.name))
      projectsByClientPair[key].push(p.name)
  }

  let batchRowsQuery = supabase
    .from('clockify_imports')
    .select('id, name, filename, imported_at')
    .eq('org_id', org.id)
    .order('imported_at', { ascending: false })
  if (viewAsScope) batchRowsQuery = batchRowsQuery.in('id', viewAsScope)
  const { data: batchRows } = await batchRowsQuery.returns<
    { id: string; name: string | null; filename: string | null; imported_at: string }[]
  >()
  const batchOptions = (batchRows ?? []).map((b) => ({
    id: b.id,
    label:
      b.name ??
      b.filename ??
      new Date(b.imported_at).toISOString().slice(0, 10),
  }))
  const batchLabel: Record<string, string> = Object.fromEntries(
    batchOptions.map((b) => [b.id, b.label]),
  )

  // When view-as constrains us to a single user, force p_imported_by to that
  // user — the URL param can't escape the view-as scope.
  const effectiveImportedBy = viewAsScope ? eu!.effective_user_id : imported_by
  const rpcArgs = {
    p_org_id: org.id,
    p_start: startBound,
    p_end: endBound,
    p_status: status,
    p_batch: batch,
    p_q: q,
    p_project: project,
    p_user: user,
    p_source_email: source_email,
    p_client: client,
    p_operator: operator,
    p_imported_by: effectiveImportedBy,
  }

  // List of importers for the FilterBar dropdown (owners / super admins only).
  // Adi's view is RLS-scoped to his own imports, so the dropdown wouldn't
  // give him anything useful.
  let importerOptions: { id: string; label: string }[] | null = null
  // Hide the Imported-by dropdown when view-as is restricting us to a single
  // user — there's nothing to switch between.
  if (userCanTransfer && !viewAsScope) {
    const { data: rawImports } = await supabase
      .from('clockify_imports')
      .select('imported_by')
      .eq('org_id', org.id)
      .not('imported_by', 'is', null)
      .returns<{ imported_by: string }[]>()
    const importerIds = Array.from(
      new Set((rawImports ?? []).map((r) => r.imported_by)),
    )
    if (importerIds.length > 0) {
      const { data: profiles } = await supabase
        .from('profiles')
        .select('id, full_name, email')
        .in('id', importerIds)
        .returns<{ id: string; full_name: string | null; email: string }[]>()
      importerOptions = (profiles ?? [])
        .map((p) => ({ id: p.id, label: p.full_name || p.email }))
        .sort((a, b) => a.label.localeCompare(b.label))
    } else {
      importerOptions = []
    }
  }

  // If filtering by importer, scope time_entries to that user's batches.
  let scopedImportIds: string[] | null = null
  if (imported_by) {
    const { data } = await supabase
      .from('clockify_imports')
      .select('id')
      .eq('org_id', org.id)
      .eq('imported_by', imported_by)
      .returns<{ id: string }[]>()
    scopedImportIds = (data ?? []).map((r) => r.id)
    if (scopedImportIds.length === 0) scopedImportIds = ['00000000-0000-0000-0000-000000000000']
  }

  const totalsPromise = supabase.rpc('entries_filter_totals', rpcArgs)
  const optionsPromise = supabase.rpc('entries_filter_options', rpcArgs)

  // Helper that mirrors the SQL-level status filter so the data query
  // returns the right rows even before any JS post-processing.
  // Post invoice-migration: "applied" (alias: "transferred") means the
  // row is attached to an invoice. We OR with transferred_at for
  // safety in case any row escaped the backfill.
  function applyStatus<T>(query: T): T {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    let qy = query as any
    const isApplied = status === 'transferred' || status === 'applied'
    if (isApplied) {
      qy = qy.or('invoice_id.not.is.null,transferred_at.not.is.null')
    } else if (status === 'pending') {
      qy = qy.is('invoice_id', null).is('transferred_at', null)
      for (const col of ELIGIBILITY_COLUMNS) {
        qy = qy.not(col, 'is', null)
      }
    } else if (status === 'blocked') {
      qy = qy.is('invoice_id', null).is('transferred_at', null)
      qy = qy.or(
        ELIGIBILITY_COLUMNS.map((c) => `${c}.is.null`).join(','),
      )
    }
    return qy as T
  }

  let countQuery = supabase
    .from('time_entries')
    .select('id', { count: 'exact', head: true })
    .eq('org_id', org.id)
  let dataQuery = supabase
    .from('time_entries')
    .select(
      'id, import_id, source_user_email, source_user_name, team_member_id, operator, client, project, description, billable, start_at, end_at, duration_seconds, converted_user, converted_duration_seconds, billout_cost_usd, billout_amount_usd, transferred_at, transfer_batch_id, invoice_id, invoice:invoices(name)',
    )
    .eq('org_id', org.id)
    .order(sortColumn, { ascending: sortDir === 'asc', nullsFirst: false })

  // Scope to a specific invoice when ?invoice=<id> is set.
  const invoiceFilter =
    typeof sp.invoice === 'string' ? sp.invoice : null
  if (invoiceFilter) {
    countQuery = countQuery.eq('invoice_id', invoiceFilter)
    dataQuery = dataQuery.eq('invoice_id', invoiceFilter)
  }

  if (startBound) {
    countQuery = countQuery.gte('start_at', startBound)
    dataQuery = dataQuery.gte('start_at', startBound)
  }
  if (endBound) {
    countQuery = countQuery.lt('start_at', endBound)
    dataQuery = dataQuery.lt('start_at', endBound)
  }
  if (batch) {
    countQuery = countQuery.eq('import_id', batch)
    dataQuery = dataQuery.eq('import_id', batch)
  }
  countQuery = applyStatus(countQuery)
  dataQuery = applyStatus(dataQuery)
  if (q) {
    const like = `%${q}%`
    countQuery = countQuery.or(
      `project.ilike.${like},description.ilike.${like},source_user_email.ilike.${like},client.ilike.${like}`,
    )
    dataQuery = dataQuery.or(
      `project.ilike.${like},description.ilike.${like},source_user_email.ilike.${like},client.ilike.${like}`,
    )
  }
  if (project) {
    countQuery = countQuery.ilike('project', `%${project}%`)
    dataQuery = dataQuery.ilike('project', `%${project}%`)
  }
  if (user) {
    countQuery = countQuery.ilike('converted_user', `%${user}%`)
    dataQuery = dataQuery.ilike('converted_user', `%${user}%`)
  }
  if (source_email) {
    countQuery = countQuery.ilike('source_user_email', `%${source_email}%`)
    dataQuery = dataQuery.ilike('source_user_email', `%${source_email}%`)
  }
  if (client) {
    countQuery = countQuery.ilike('client', `%${client}%`)
    dataQuery = dataQuery.ilike('client', `%${client}%`)
  }
  if (operator) {
    countQuery = countQuery.ilike('operator', `%${operator}%`)
    dataQuery = dataQuery.ilike('operator', `%${operator}%`)
  }
  if (scopedImportIds) {
    countQuery = countQuery.in('import_id', scopedImportIds)
    dataQuery = dataQuery.in('import_id', scopedImportIds)
  }
  if (viewAsScope) {
    countQuery = countQuery.in('import_id', viewAsScope)
    dataQuery = dataQuery.in('import_id', viewAsScope)
  }

  const offset = (page - 1) * PAGE_SIZE
  type RawEntryRow = Omit<Entry, 'invoice_name'> & {
    invoice?: { name: string | null } | null
  }
  const [{ count: rawCount }, dataResult, totalsResult, optionsResult] =
    await Promise.all([
      countQuery,
      dataQuery.range(offset, offset + PAGE_SIZE - 1).returns<RawEntryRow[]>(),
      totalsPromise,
      optionsPromise,
    ])
  const entries: Entry[] = (dataResult.data ?? []).map((r) => ({
    ...r,
    invoice_name: r.invoice?.name ?? null,
  }))

  const totalsRow = (totalsResult.data as TotalsRow[] | null)?.[0]
  const totals = {
    rowCount: Number(totalsRow?.row_count ?? rawCount ?? 0),
    sourceSeconds: Number(totalsRow?.source_seconds ?? 0),
    billoutSeconds: Number(totalsRow?.billout_seconds ?? 0),
    cost: Number(totalsRow?.cost_usd ?? 0),
    billoutAmount: Number(totalsRow?.billout_amount_usd ?? 0),
  }

  const optionsRow = (optionsResult.data as OptionsRow[] | null)?.[0]
  const filterOptions: FilterOptions = {
    operators: optionsRow?.operators ?? [],
    clients: optionsRow?.clients ?? [],
    projects: optionsRow?.projects ?? [],
    users: optionsRow?.users ?? [],
    source_emails: optionsRow?.source_emails ?? [],
  }

  const totalCount = rawCount ?? 0
  const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE))

  const pageUrl = (n: number) => {
    const next = new URLSearchParams(sp as Record<string, string>)
    next.set('page', String(n))
    return `/entries?${next.toString()}`
  }

  const filterSnapshot: FilterSnapshot = {
    date: typeof sp.date === 'string' ? sp.date : undefined,
    start: typeof sp.start === 'string' ? sp.start : undefined,
    end: typeof sp.end === 'string' ? sp.end : undefined,
    status: typeof sp.status === 'string' ? sp.status : undefined,
    batch: typeof sp.batch === 'string' ? sp.batch : undefined,
    q: typeof sp.q === 'string' ? sp.q : undefined,
    project: typeof sp.project === 'string' ? sp.project : undefined,
    user: typeof sp.user === 'string' ? sp.user : undefined,
    source_email:
      typeof sp.source_email === 'string' ? sp.source_email : undefined,
    client: typeof sp.client === 'string' ? sp.client : undefined,
    operator: typeof sp.operator === 'string' ? sp.operator : undefined,
    imported_by:
      typeof sp.imported_by === 'string' ? sp.imported_by : undefined,
  }

  return (
    <main>
      <div className="page-head">
        <div>
          <h1>Entries</h1>
          <p className="subtitle">
            Every imported time entry, with filters, bulk transfer marking,
            and per-row edit. The summary card reflects everything that
            matches the filter (across all pages).
          </p>
        </div>
        {canEdit && !showNewEntry && (
          <div className="page-head-actions">
            <Link
              href={(() => {
                const next = new URLSearchParams()
                for (const [k, v] of Object.entries(sp)) {
                  if (typeof v === 'string') next.set(k, v)
                }
                next.set('new', '1')
                return `/entries?${next.toString()}`
              })()}
              scroll={false}
            >
              + New entry
            </Link>
          </div>
        )}
      </div>

      {error && <p className="error">{error}</p>}
      {info && <p className="info">{info}</p>}
      {markedBatch && (
        <p className="info">
          ✓ Marked {opCount ?? '?'} entries as transferred (batch{' '}
          <code>{markedBatch.slice(0, 8)}</code>).
        </p>
      )}
      {unmarked && (
        <p className="info">
          ✓ Unmarked {opCount ?? '?'} entries — they&apos;re back in the
          pending list.
        </p>
      )}
      {bulkEdited != null && (
        <p className="info">
          ✓ Bulk-updated {bulkEdited} {bulkEdited === 1 ? 'entry' : 'entries'}.
        </p>
      )}
      {recalculated != null && (
        <p className="info">
          ✓ Recalculated cost + billout for {recalculated}{' '}
          {recalculated === 1 ? 'entry' : 'entries'} using current rates.
        </p>
      )}

      <FilterBar
        showStatus
        showBatch
        showQuery
        showPivots
        showClear
        defaultDate="all"
        defaultStatus="pending"
        batches={batchOptions}
        options={filterOptions}
        importers={importerOptions}
      />

      <EntriesSummary
        rowCount={totals.rowCount}
        sourceSeconds={totals.sourceSeconds}
        cost={totals.cost}
        billoutAmount={userCanTransfer ? totals.billoutAmount : null}
      />

      <div
        style={{
          padding: '0.25rem 0 1rem',
          display: 'flex',
          justifyContent: 'space-between',
          alignItems: 'center',
          flexWrap: 'wrap',
          gap: '0.5rem',
        }}
      >
        <div className="muted" style={{ fontSize: '0.9em' }}>
          Showing {entries.length} of {totalCount} on this page
        </div>
        {totalPages > 1 && (
          <div className="flex-row" style={{ gap: '0.5rem' }}>
            {page > 1 && <Link href={pageUrl(page - 1)}>← Prev</Link>}
            <span className="muted">
              Page {page} of {totalPages}
            </span>
            {page < totalPages && (
              <Link href={pageUrl(page + 1)}>Next →</Link>
            )}
          </div>
        )}
      </div>

      {entries.length === 0 && !showNewEntry ? (
        <p className="empty">No entries match this filter.</p>
      ) : (
        <EntriesTable
          entries={entries}
          totalCount={totalCount}
          editId={editId}
          canEdit={canEdit}
          canTransfer={userCanTransfer}
          filter={filterSnapshot}
          batchLabel={batchLabel}
          operatorNames={operatorNames}
          clientNames={clientNames}
          projectNames={projectNames}
          clientNameToOperators={clientNameToOperators}
          projectNameToParents={projectNameToParents}
          clientsByOperator={clientsByOperator}
          projectsByClientPair={projectsByClientPair}
          teamMembers={teamMembers}
          projectOptions={projectOptions}
          invoiceOptions={invoiceOptions}
          showNewEntry={showNewEntry}
          currentUserEmail={eu?.email ?? ''}
          currentUserName={eu?.full_name ?? eu?.email ?? ''}
        />
      )}

      {totalPages > 1 && (
        <div
          style={{
            padding: '0.75rem 0',
            display: 'flex',
            justifyContent: 'space-between',
            alignItems: 'center',
          }}
        >
          <span className="muted" style={{ fontSize: '0.85em' }}>
            Page {page} of {totalPages}
          </span>
          <div className="flex-row" style={{ gap: '0.5rem' }}>
            {page > 1 && (
              <Link href={pageUrl(page - 1)} className="button-link-secondary">
                ← Prev
              </Link>
            )}
            {page < totalPages && (
              <Link href={pageUrl(page + 1)} className="button-link-secondary">
                Next →
              </Link>
            )}
          </div>
        </div>
      )}
    </main>
  )
}
