import 'server-only'

import { createHash } from 'node:crypto'
import { createAdminClient } from '@/lib/supabase/admin'
import type { SessionUser } from '@/lib/auth'
import { getCompanyContext } from '@/lib/data/companies'
import { findSelfAccountById } from '@/lib/auth/self-accounts'
import {
  DEFAULT_INTAKE_LIMITS, DEFAULT_INTAKE_POLICY, DENIAL_MESSAGES, isDisposableEmail, riskDecision, riskScore, shouldEnterProtection,
  submissionDecision, trustLevel, validateIntakeFiles, type IntakeLimits, type IntakePolicy, type TrustLevel,
} from '@/lib/domain/intake-security'
import { logSecurityEvent } from '@/lib/security/events'

/**
 * Server-side intake guard (2026-09-22, §36): every rule enforced here, never
 * in the browser. Gathers limits, blocks, invitation, counts, file checks,
 * hashes and risk, records the attempt in intake_submissions, and answers
 * allow / deny. Tolerates migration 0029 not being applied (defaults; the
 * attempt record then falls back to the 0001 columns).
 */
const missing = (e: { message?: string } | null | undefined) => !!e && /does not exist|schema cache|column/i.test(e.message ?? '')
const norm = (e: string) => e.trim().toLowerCase()

export async function intakeLimits(): Promise<IntakeLimits> {
  const { data } = await createAdminClient().from('intake_settings').select('*').eq('id', 1).maybeSingle()
  return data ? { ...DEFAULT_INTAKE_LIMITS, ...(data as Partial<IntakeLimits>) } : DEFAULT_INTAKE_LIMITS
}

export function policyFromPage(page: Record<string, unknown>): IntakePolicy {
  const p = { ...DEFAULT_INTAKE_POLICY }
  for (const k of Object.keys(p) as (keyof IntakePolicy)[]) if (page[k] !== undefined && page[k] !== null) (p as Record<string, unknown>)[k] = page[k]
  if (page.max_files === null) p.max_files = null
  return p
}

export function protectionActive(policy: IntakePolicy, now = Date.now()): boolean {
  return policy.protection_mode && (!policy.protection_until || Date.parse(policy.protection_until) > now)
}

/** Cloudflare Turnstile (§18): only enforced when the secret is configured. */
export async function verifyBotCheck(token: string | null, ip: string | null): Promise<{ configured: boolean; passed: boolean }> {
  const secret = process.env.TURNSTILE_SECRET_KEY?.trim()
  if (!secret) return { configured: false, passed: true }
  if (!token) return { configured: true, passed: false }
  try {
    const res = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ secret, response: token, ...(ip && ip !== 'unknown' ? { remoteip: ip } : {}) }), signal: AbortSignal.timeout(8_000),
    })
    const json = (await res.json().catch(() => ({}))) as { success?: boolean }
    return { configured: true, passed: !!json.success }
  } catch {
    return { configured: true, passed: false }
  }
}

export async function isBlocked(params: { ip: string | null; email: string | null; userId: string | null }): Promise<boolean> {
  const admin = createAdminClient()
  const ors: string[] = []
  if (params.ip) ors.push(`and(kind.eq.ip,value.eq.${params.ip})`)
  if (params.email) ors.push(`and(kind.eq.email,value.eq.${norm(params.email)})`)
  if (params.userId) ors.push(`and(kind.eq.user,value.eq.${params.userId})`)
  if (!ors.length) return false
  const { data, error } = await admin.from('intake_blocks').select('until').or(ors.join(','))
  if (error || !data?.length) return false
  return data.some((b) => !b.until || Date.parse(b.until) > Date.now())
}

export interface InvitationMatch { id: string; email: string; brokerPageId: string }

/** A secure invitation link (§15): valid, unexpired, for this page. */
export async function matchInvitation(token: string | null, brokerPageId: string): Promise<InvitationMatch | null> {
  if (!token) return null
  const hash = createHash('sha256').update(token).digest('hex')
  const { data } = await createAdminClient().from('intake_invitations').select('id, email_normalized, broker_page_id, expires_at, used_at').eq('token_hash', hash).maybeSingle()
  if (!data || data.broker_page_id !== brokerPageId || Date.parse(data.expires_at) < Date.now()) return null
  return { id: data.id, email: data.email_normalized, brokerPageId: data.broker_page_id }
}

async function counts(pageId: string, userId: string | null, ip: string | null) {
  const admin = createAdminClient()
  const dayAgo = new Date(Date.now() - 86_400_000).toISOString(), hourAgo = new Date(Date.now() - 3_600_000).toISOString()
  const head = { count: 'exact' as const, head: true }
  const [u, i, p, acc, vf] = await Promise.all([
    userId ? admin.from('intake_submissions').select('id', head).eq('user_id', userId).gte('created_at', dayAgo).in('status', ['accepted', 'received', 'review_required']) : Promise.resolve({ count: 0 }),
    ip ? admin.from('intake_submissions').select('id', head).eq('source_ip', ip).gte('created_at', hourAgo) : Promise.resolve({ count: 0 }),
    admin.from('intake_submissions').select('id', head).eq('broker_page_id', pageId).gte('created_at', hourAgo),
    ip ? admin.from('security_events').select('id', head).eq('ip', ip).eq('event', 'login_success').gte('created_at', dayAgo) : Promise.resolve({ count: 0 }),
    ip ? admin.from('security_events').select('id', head).eq('ip', ip).eq('event', 'login_mfa_failed').gte('created_at', hourAgo) : Promise.resolve({ count: 0 }),
  ])
  return { userToday: u.count ?? 0, ipLastHour: i.count ?? 0, pageLastHour: p.count ?? 0, ipAccountsLastDay: acc.count ?? 0, recentVerificationFailures: vf.count ?? 0 }
}

export interface GuardInput {
  page: Record<string, unknown> & { id: string; slug: string }
  user: SessionUser | null
  ip: string | null
  userAgent: string | null
  honeypotFilled: boolean
  botToken: string | null
  invitationToken: string | null
  files: File[]
  fields: { carrier_company: string; contact_name: string; contact_email: string; contact_phone: string; permit_source: string; notes: string | null }
}

export type GuardResult =
  | { ok: true; submissionId: string | null; level: TrustLevel; invitation: InvitationMatch | null; fileHashes: string[]; review: boolean; policy: IntakePolicy; limits: IntakeLimits }
  | { ok: false; status: number; error: string; code: string; submissionId: string | null }

export async function guardIntakeSubmission(input: GuardInput): Promise<GuardResult> {
  const admin = createAdminClient()
  const limits = await intakeLimits()
  const policy = policyFromPage(input.page)
  const user = input.user
  const email = user?.email ?? input.fields.contact_email
  const self = user ? await findSelfAccountById(user.id) : null
  const emailVerified = !!user && (self ? !!self.email_verified_at : true) // env logins are admin-provisioned = verified
  const companyCtx = user && (user.role === 'dispatcher' || user.role === 'broker') ? await getCompanyContext(user.id) : null
  const companyVerified = companyCtx?.membership?.status === 'approved'
  const invitation = await matchInvitation(input.invitationToken, input.page.id)
  const invited = !!invitation && !!user && norm(invitation.email) === norm(user.email)
  const trusted = (self?.intake_trust_level ?? 0) >= 4
  const level = trustLevel({ signedIn: !!user, emailVerified, companyVerified, invited, trusted, isAdmin: user?.role === 'admin' })
  const blocked = await isBlocked({ ip: input.ip, email, userId: user?.id ?? null })
  const c = await counts(input.page.id, user?.id ?? null, input.ip)
  const protection = protectionActive(policy)

  const record = async (status: string, extra: Record<string, unknown>): Promise<string | null> => {
    const base = { broker_page_id: input.page.id, carrier_company: input.fields.carrier_company, contact_name: input.fields.contact_name, contact_email: input.fields.contact_email, contact_phone: input.fields.contact_phone, permit_source: input.fields.permit_source, notes: input.fields.notes, source_ip: input.ip }
    let r = await admin.from('intake_submissions').insert({ ...base, user_id: user?.id ?? null, status, trust_level: level, user_agent: input.userAgent?.slice(0, 300) ?? null, invitation_id: invitation?.id ?? null, ...extra }).select('id').single()
    if (r.error && missing(r.error) && status === 'accepted') r = await admin.from('intake_submissions').insert(base).select('id').single()
    return r.data?.id ?? null
  }
  const deny = async (code: string, status: number, error: string, recordAs: string, extra: Record<string, unknown> = {}) => {
    const id = recordAs === 'none' ? null : await record(recordAs, { reason: code, ...extra })
    await logSecurityEvent({ event: 'api_rate_limited', username: email, userId: user?.id ?? null, ip: input.ip, userAgent: input.userAgent, detail: { kind: 'intake_denied', code, page: input.page.slug, submission_id: id } })
    return { ok: false as const, status, error, code, submissionId: id }
  }

  // Honeypot (§19): pretend success, keep nothing but the log.
  if (input.honeypotFilled) return deny('honeypot', 200, 'ok', 'blocked', { risk_score: 100, risk_signals: { honeypot: true } })

  const decision = submissionDecision({ level, isAdmin: user?.role === 'admin', blocked, policy, limits, counts: c, protectionActive: protection, invited })
  if (!decision.allowed) {
    const status = decision.reason === 'auth_required' || decision.reason === 'email_verification_required' ? 401 : decision.reason === 'blocked' ? 403 : ['daily_quota', 'ip_velocity', 'page_velocity', 'protection_mode'].includes(decision.reason) ? 429 : 403
    return deny(decision.reason, status, DENIAL_MESSAGES[decision.reason], decision.reason === 'auth_required' ? 'none' : decision.reason === 'blocked' ? 'blocked' : 'rejected')
  }

  // Bot check (§18) — only when Turnstile is configured.
  const bot = await verifyBotCheck(input.botToken, input.ip)
  if (bot.configured && !bot.passed && user?.role !== 'admin') return deny('bot_check', 403, 'We could not confirm you are not a bot. Reload the page and try again.', 'blocked', { risk_signals: { bot_check_failed: true } })

  // Files (§20): count, sizes, real type.
  const heads = await Promise.all(input.files.map(async (f) => ({ name: f.name, size: f.size, head: new Uint8Array(await f.slice(0, 8).arrayBuffer()) })))
  const fileCheck = validateIntakeFiles(heads, limits, policy)
  if (!fileCheck.ok) return deny('file_rejected', 400, fileCheck.error, 'rejected', { file_count: input.files.length, total_bytes: heads.reduce((n, h) => n + h.size, 0) })

  // Hashes + duplicates (§24).
  const fileHashes = await Promise.all(input.files.map(async (f) => createHash('sha256').update(Buffer.from(await f.arrayBuffer())).digest('hex')))
  const submissionHash = createHash('sha256').update([input.page.id, norm(email), ...fileHashes].join('|')).digest('hex')
  const since = new Date(Date.now() - limits.duplicate_window_hours * 3_600_000).toISOString()
  const { data: dupRows } = await admin.from('intake_submissions').select('id, status').eq('submission_hash', submissionHash).gte('created_at', since).in('status', ['accepted', 'received', 'review_required']).limit(1)
  if (dupRows?.length) return deny('duplicate', 409, 'This exact submission was already received. Open your trip workspace instead of sending it again.', 'duplicate', { file_hashes: fileHashes, submission_hash: submissionHash })
  const { count: repeatedFiles } = fileHashes.length
    ? await admin.from('intake_submissions').select('id', { count: 'exact', head: true }).overlaps('file_hashes', fileHashes).gte('created_at', since)
    : { count: 0 }

  // Risk (§26).
  const risk = riskScore({
    level, accountAgeHours: self ? (Date.now() - Date.parse(self.created_at)) / 3_600_000 : null, userToday: c.userToday, ipLastHour: c.ipLastHour,
    ipAccountsLastDay: c.ipAccountsLastDay, disposableEmail: isDisposableEmail(email), duplicateFiles: repeatedFiles ?? 0,
    botCheckFailed: bot.configured && !bot.passed, recentVerificationFailures: c.recentVerificationFailures, honeypotFilled: false,
  })
  const verdict = user?.role === 'admin' ? 'allow' : riskDecision(risk.score, limits)
  if (verdict === 'block') return deny('risk_block', 403, 'This submission could not be accepted automatically. HeavyHaul Agent support has been notified.', 'blocked', { risk_score: risk.score, risk_signals: risk.reasons, file_hashes: fileHashes, submission_hash: submissionHash })

  const submissionId = await record(verdict === 'review' ? 'review_required' : 'received', {
    risk_score: risk.score, risk_signals: risk.reasons, file_count: input.files.length, total_bytes: heads.reduce((n, h) => n + h.size, 0), file_hashes: fileHashes, submission_hash: submissionHash,
    processing_status: verdict === 'review' ? 'skipped' : 'queued',
  })

  // Protection mode (§25): this page's own velocity.
  if (!protection && shouldEnterProtection(c.pageLastHour + 1, limits)) {
    await admin.from('broker_pages').update({ protection_mode: true, protection_reason: `${c.pageLastHour + 1} submissions in an hour`, protection_until: new Date(Date.now() + limits.protection_hours * 3_600_000).toISOString() }).eq('id', input.page.id)
    await logSecurityEvent({ event: 'api_rate_limited', ip: input.ip, detail: { kind: 'intake_protection_mode', page: input.page.slug, per_hour: c.pageLastHour + 1 } })
  }
  if (invitation && user) await admin.from('intake_invitations').update({ used_at: new Date().toISOString(), used_by_user_id: user.id }).eq('id', invitation.id).is('used_at', null)
  await logSecurityEvent({ event: 'login_success', username: email, userId: user?.id ?? null, ip: input.ip, userAgent: input.userAgent, detail: { kind: 'intake_accepted', page: input.page.slug, level, risk: risk.score, review: verdict === 'review', submission_id: submissionId } })
  return { ok: true, submissionId, level, invitation, fileHashes, review: verdict === 'review', policy, limits }
}

export async function finalizeIntakeSubmission(submissionId: string | null, patch: { trip_id?: string; status?: string; processing_status?: string; reason?: string }) {
  if (!submissionId) return
  const { error } = await createAdminClient().from('intake_submissions').update(patch).eq('id', submissionId)
  if (error && missing(error) && patch.trip_id) await createAdminClient().from('intake_submissions').update({ trip_id: patch.trip_id }).eq('id', submissionId)
}
