import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSessionUser } from '@/lib/auth'
import { createAdminClient } from '@/lib/supabase/admin'
import {
  approveClaim, denyClaim, logCompanyEvent, raiseVerification,
} from '@/lib/data/companies'
import { normalizeMc } from '@/lib/domain/company'
import type { CompanyClaim } from '@/types/db'

/**
 * Manual review queue actions (Task 93; article §8 Method 4, §19).
 * Admin-gated, server-enforced. Everything lands in company_audit_log.
 *
 * ops:
 *   approve_company  approve the claim manually → level 4, first owner allowed
 *   reject           deny the claim
 *   request_info     ask the user for more (shown on their Settings card)
 *   assign_owner     make a user the Company Owner directly
 *   merge            fold a duplicate company into the surviving one
 *   lock             lock / unlock a suspicious profile
 *   note             append reviewer notes to a claim
 *   checklist        §8 evidence checklist → company_verification_records
 *   company_patch    edit the company profile from the queue
 */

const schema = z.discriminatedUnion('op', [
  z.object({ op: z.literal('approve_company'), claim_id: z.string().uuid() }),
  z.object({ op: z.literal('reject'), claim_id: z.string().uuid(), note: z.string().trim().max(1000).optional() }),
  z.object({ op: z.literal('request_info'), claim_id: z.string().uuid(), note: z.string().trim().min(1).max(1000) }),
  z.object({ op: z.literal('assign_owner'), company_id: z.string().uuid(), user_id: z.string().uuid() }),
  z.object({ op: z.literal('merge'), source_company_id: z.string().uuid(), target_company_id: z.string().uuid() }),
  z.object({ op: z.literal('lock'), company_id: z.string().uuid(), locked: z.boolean() }),
  z.object({ op: z.literal('note'), claim_id: z.string().uuid(), note: z.string().trim().min(1).max(2000) }),
  z.object({
    op: z.literal('checklist'),
    company_id: z.string().uuid(),
    items: z.record(z.string(), z.boolean()),
    notes: z.string().trim().max(2000).optional(),
  }),
  z.object({
    op: z.literal('company_patch'),
    company_id: z.string().uuid(),
    legal_name: z.string().trim().min(1).max(200),
    display_name: z.string().trim().min(1).max(160),
    mc_number: z.string().trim().max(20),
    dot_number: z.string().trim().max(20),
    corporate_email: z.string().trim().max(200),
    corporate_phone: z.string().trim().max(40),
    website: z.string().trim().max(200),
    physical_address: z.string().trim().max(400),
  }),
])

export async function POST(req: NextRequest) {
  const user = await getSessionUser()
  if (!user) return NextResponse.json({ error: 'Sign in first.' }, { status: 401 })
  if (user.role !== 'admin') return NextResponse.json({ error: 'Admin only.' }, { status: 403 })
  const parsed = schema.safeParse(await req.json().catch(() => ({})))
  if (!parsed.success) return NextResponse.json({ error: 'Invalid request.' }, { status: 400 })
  const input = parsed.data

  const admin = createAdminClient()
  const actorLabel = `${user.name || user.email} (HeavyHaul Agent admin)`
  const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? null
  const now = new Date().toISOString()

  async function loadClaim(id: string): Promise<CompanyClaim | null> {
    const { data } = await admin.from('company_claims').select('*').eq('id', id).maybeSingle()
    return (data ?? null) as CompanyClaim | null
  }

  switch (input.op) {
    case 'approve_company': {
      const claim = await loadClaim(input.claim_id)
      if (!claim) return NextResponse.json({ error: 'Claim not found.' }, { status: 404 })
      if (claim.review_reason === 'mc_number_change') {
        // The MC change itself is the decision (§20 "MC number change requires admin review").
        const { data: company } = await admin.from('companies').select('mc_number').eq('id', claim.company_id).maybeSingle()
        await admin.from('companies').update({ mc_number: claim.mc_number, updated_at: now }).eq('id', claim.company_id)
        await admin.from('company_claims').update({ claim_status: 'approved', decided_at: now, approver_name: actorLabel, updated_at: now }).eq('id', claim.id)
        await logCompanyEvent({ companyId: claim.company_id, actorUserId: user.id, actorLabel, eventType: 'mc_change_approved', oldValue: { mc_number: company?.mc_number ?? null }, newValue: { mc_number: claim.mc_number }, ip })
        return NextResponse.json({ ok: true })
      }
      if (['approved', 'rejected', 'revoked'].includes(claim.claim_status)) {
        return NextResponse.json({ error: 'This request has already been decided.' }, { status: 409 })
      }
      const result = await approveClaim({ claim, approverLabel: actorLabel, approverUserId: user.id, approverEmail: user.email, ip, via: 'manual' })
      if (!result.ok) return NextResponse.json({ error: result.error }, { status: 400 })
      return NextResponse.json({ ok: true, role: result.role })
    }

    case 'reject': {
      const claim = await loadClaim(input.claim_id)
      if (!claim) return NextResponse.json({ error: 'Claim not found.' }, { status: 404 })
      if (input.note) {
        await admin.from('company_claims').update({ review_notes: appendNote(claim.review_notes, actorLabel, input.note) }).eq('id', claim.id)
      }
      await denyClaim({ claim, approverLabel: actorLabel, approverUserId: user.id, approverEmail: user.email, ip })
      return NextResponse.json({ ok: true })
    }

    case 'request_info': {
      const claim = await loadClaim(input.claim_id)
      if (!claim) return NextResponse.json({ error: 'Claim not found.' }, { status: 404 })
      await admin
        .from('company_claims')
        .update({ claim_status: 'company_verification_pending', info_request: input.note, updated_at: now })
        .eq('id', claim.id)
      await logCompanyEvent({ companyId: claim.company_id, actorUserId: user.id, actorLabel, eventType: 'info_requested', newValue: { claim_id: claim.id, note: input.note }, ip })
      return NextResponse.json({ ok: true })
    }

    case 'assign_owner': {
      await admin.from('company_memberships').upsert(
        { user_id: input.user_id, company_id: input.company_id, role: 'company_owner', status: 'approved', approved_by: actorLabel, approved_at: now, revoked_by: null, revoked_at: null },
        { onConflict: 'user_id,company_id' },
      )
      await admin
        .from('company_claims')
        .update({ claim_status: 'approved', decided_at: now, approver_name: actorLabel, updated_at: now })
        .eq('user_id', input.user_id)
        .eq('company_id', input.company_id)
        .not('claim_status', 'in', '("approved","rejected","revoked")')
      await raiseVerification(input.company_id, 'manual_verified')
      await admin.from('companies').update({ is_placeholder: false, updated_at: now }).eq('id', input.company_id)
      await logCompanyEvent({ companyId: input.company_id, actorUserId: user.id, actorLabel, eventType: 'owner_assigned', newValue: { user_id: input.user_id }, ip })
      return NextResponse.json({ ok: true })
    }

    case 'merge': {
      if (input.source_company_id === input.target_company_id) {
        return NextResponse.json({ error: 'Pick two different companies.' }, { status: 400 })
      }
      const [{ data: source }, { data: target }] = await Promise.all([
        admin.from('companies').select('id, display_name, mc_number').eq('id', input.source_company_id).maybeSingle(),
        admin.from('companies').select('id, display_name').eq('id', input.target_company_id).maybeSingle(),
      ])
      if (!source || !target) return NextResponse.json({ error: 'Company not found.' }, { status: 404 })
      // Memberships that would collide keep the target's row; the rest move.
      const { data: targetMembers } = await admin.from('company_memberships').select('user_id').eq('company_id', target.id)
      const keep = new Set((targetMembers ?? []).map((m) => m.user_id))
      const { data: sourceMembers } = await admin.from('company_memberships').select('id, user_id').eq('company_id', source.id)
      for (const m of sourceMembers ?? []) {
        if (keep.has(m.user_id)) await admin.from('company_memberships').update({ status: 'revoked', revoked_by: actorLabel, revoked_at: now }).eq('id', m.id)
        else await admin.from('company_memberships').update({ company_id: target.id }).eq('id', m.id)
      }
      await admin.from('company_claims').update({ company_id: target.id }).eq('company_id', source.id)
      await admin.from('company_verification_records').update({ company_id: target.id }).eq('company_id', source.id)
      await admin.from('broker_pages').update({ company_id: target.id }).eq('company_id', source.id)
      // The MC unique index excludes merged rows, so the target may keep the number.
      await admin.from('companies').update({ merged_into: target.id, locked: true, updated_at: now }).eq('id', source.id)
      await logCompanyEvent({ companyId: target.id, actorUserId: user.id, actorLabel, eventType: 'company_merged', oldValue: { merged: source.id, name: source.display_name }, newValue: { into: target.id }, ip })
      await logCompanyEvent({ companyId: source.id, actorUserId: user.id, actorLabel, eventType: 'company_merged_away', newValue: { into: target.id }, ip })
      return NextResponse.json({ ok: true })
    }

    case 'lock': {
      await admin.from('companies').update({ locked: input.locked, updated_at: now }).eq('id', input.company_id)
      await logCompanyEvent({ companyId: input.company_id, actorUserId: user.id, actorLabel, eventType: input.locked ? 'company_locked' : 'company_unlocked', ip })
      return NextResponse.json({ ok: true })
    }

    case 'note': {
      const claim = await loadClaim(input.claim_id)
      if (!claim) return NextResponse.json({ error: 'Claim not found.' }, { status: 404 })
      await admin.from('company_claims').update({ review_notes: appendNote(claim.review_notes, actorLabel, input.note), updated_at: now }).eq('id', claim.id)
      await logCompanyEvent({ companyId: claim.company_id, actorUserId: user.id, actorLabel, eventType: 'review_note', newValue: { claim_id: claim.id, note: input.note }, ip })
      return NextResponse.json({ ok: true })
    }

    case 'checklist': {
      const checked = Object.entries(input.items).filter(([, v]) => v).map(([k]) => k)
      await admin.from('company_verification_records').insert({
        company_id: input.company_id,
        source: 'manual',
        source_data: { checklist: input.items, notes: input.notes ?? null, by: actorLabel },
        match_score: checked.length >= 3 ? 'high' : checked.length > 0 ? 'medium' : 'none',
        verified_fields: checked,
        verified_at: now,
        status: 'recorded',
      })
      await logCompanyEvent({ companyId: input.company_id, actorUserId: user.id, actorLabel, eventType: 'evidence_recorded', newValue: { checked }, ip })
      return NextResponse.json({ ok: true })
    }

    case 'company_patch': {
      const mc = input.mc_number ? normalizeMc(input.mc_number) : null
      if (input.mc_number && !mc) return NextResponse.json({ error: 'MC number must be 5–8 digits.' }, { status: 400 })
      const { data: before } = await admin.from('companies').select('*').eq('id', input.company_id).maybeSingle()
      const patch = {
        legal_name: input.legal_name,
        display_name: input.display_name,
        mc_number: mc,
        dot_number: input.dot_number || null,
        corporate_email: input.corporate_email || null,
        corporate_phone: input.corporate_phone || null,
        website: input.website || null,
        physical_address: input.physical_address || null,
        updated_at: now,
      }
      const { error } = await admin.from('companies').update(patch).eq('id', input.company_id)
      if (error) return NextResponse.json({ error: error.message.includes('companies_mc_number_key') ? 'Another company already has this MC number.' : 'Could not save.' }, { status: 400 })
      await logCompanyEvent({ companyId: input.company_id, actorUserId: user.id, actorLabel, eventType: 'profile_updated_by_admin', oldValue: before ? pick(before, Object.keys(patch)) : null, newValue: patch, ip })
      return NextResponse.json({ ok: true })
    }
  }
}

function appendNote(existing: string | null, who: string, note: string): string {
  const line = `[${new Date().toISOString().slice(0, 16).replace('T', ' ')}] ${who}: ${note}`
  return existing ? `${existing}\n${line}` : line
}

function pick(obj: Record<string, unknown>, keys: string[]) {
  const out: Record<string, unknown> = {}
  for (const k of keys) if (k !== 'updated_at') out[k] = obj[k] ?? null
  return out
}
