/**
 * Public broker intake — security rules (Nash, 2026-09-22). Pure; the
 * intake route gathers the counts and calls these.
 *
 *   Level 0  anonymous          may view the page, never complete a submission
 *   Level 1  account, unverified 1 submission / 24 h, small files
 *   Level 2  email verified     normal free limits
 *   Level 3  company verified   higher limits
 *   Level 4  invited / trusted  lowest friction
 */

export type TrustLevel = 0 | 1 | 2 | 3 | 4

export interface IntakeLimits {
  unverified_per_day: number
  verified_per_day: number
  company_verified_per_day: number
  invited_per_day: number
  ip_per_hour: number
  page_per_hour: number
  page_protection_per_hour: number
  protection_hours: number
  verification_emails_per_15min: number
  max_files: number
  max_file_mb: number
  max_total_mb: number
  duplicate_window_hours: number
  review_risk_score: number
  block_risk_score: number
}

export const DEFAULT_INTAKE_LIMITS: IntakeLimits = {
  unverified_per_day: 1, verified_per_day: 10, company_verified_per_day: 50, invited_per_day: 100,
  ip_per_hour: 20, page_per_hour: 200, page_protection_per_hour: 500, protection_hours: 6,
  verification_emails_per_15min: 3, max_files: 10, max_file_mb: 20, max_total_mb: 75,
  duplicate_window_hours: 24, review_risk_score: 50, block_risk_score: 80,
}

export interface IntakePolicy {
  intake_enabled: boolean
  who_can_submit: 'verified_email' | 'verified_company' | 'invited_only'
  allow_permit_upload: boolean
  allow_rate_confirmation_upload: boolean
  max_files: number | null
  max_file_mb: number | null
  max_total_mb: number | null
  require_carrier_mc: boolean
  require_phone: boolean
  submissions_per_hour_limit: number | null
  protection_mode: boolean
  protection_until: string | null
}

export const DEFAULT_INTAKE_POLICY: IntakePolicy = {
  intake_enabled: true, who_can_submit: 'verified_email', allow_permit_upload: true, allow_rate_confirmation_upload: true,
  max_files: null, max_file_mb: null, max_total_mb: null, require_carrier_mc: false, require_phone: true,
  submissions_per_hour_limit: null, protection_mode: false, protection_until: null,
}

export function trustLevel(p: { signedIn: boolean; emailVerified: boolean; companyVerified: boolean; invited: boolean; trusted: boolean; isAdmin: boolean }): TrustLevel {
  if (!p.signedIn) return 0
  if (p.isAdmin || p.invited || p.trusted) return 4
  if (p.companyVerified) return 3
  if (p.emailVerified) return 2
  return 1
}

export function dailyQuotaFor(level: TrustLevel, limits: IntakeLimits): number {
  switch (level) {
    case 0: return 0
    case 1: return limits.unverified_per_day
    case 2: return limits.verified_per_day
    case 3: return limits.company_verified_per_day
    case 4: return limits.invited_per_day
  }
}

export type IntakeDenial =
  | 'auth_required' | 'blocked' | 'intake_disabled' | 'protection_mode' | 'not_invited' | 'company_verification_required'
  | 'email_verification_required' | 'daily_quota' | 'ip_velocity' | 'page_velocity'

export function submissionDecision(params: {
  level: TrustLevel
  isAdmin: boolean
  blocked: boolean
  policy: IntakePolicy
  limits: IntakeLimits
  counts: { userToday: number; ipLastHour: number; pageLastHour: number }
  protectionActive: boolean
  invited: boolean
  now?: number
}): { allowed: true } | { allowed: false; reason: IntakeDenial } {
  const { level, policy, limits, counts } = params
  if (params.blocked) return { allowed: false, reason: 'blocked' }
  if (level === 0) return { allowed: false, reason: 'auth_required' }
  if (!policy.intake_enabled) return { allowed: false, reason: 'intake_disabled' }
  if (params.isAdmin) return { allowed: true }
  // A page under attack accepts invited/trusted people only until the window passes (§25).
  if (params.protectionActive && level < 4) return { allowed: false, reason: 'protection_mode' }
  if (policy.who_can_submit === 'invited_only' && !params.invited) return { allowed: false, reason: 'not_invited' }
  if (policy.who_can_submit === 'verified_company' && level < 3) return { allowed: false, reason: 'company_verification_required' }
  // §38: the final submission needs a verified email.
  if (level < 2) return { allowed: false, reason: 'email_verification_required' }
  if (counts.userToday >= dailyQuotaFor(level, limits)) return { allowed: false, reason: 'daily_quota' }
  if (counts.ipLastHour >= limits.ip_per_hour) return { allowed: false, reason: 'ip_velocity' }
  const pageLimit = policy.submissions_per_hour_limit ?? limits.page_per_hour
  if (counts.pageLastHour >= pageLimit) return { allowed: false, reason: 'page_velocity' }
  return { allowed: true }
}

/** §25: a page's own counter crossing the protection line switches Intake Protection Mode on. */
export function shouldEnterProtection(pageLastHour: number, limits: IntakeLimits): boolean {
  return pageLastHour >= limits.page_protection_per_hour
}

/* ---------------- files (§20) ---------------- */

export const INTAKE_FILE_TYPES: Record<string, { ext: string[]; magic: (b: Uint8Array) => boolean }> = {
  'application/pdf': { ext: ['pdf'], magic: (b) => b[0] === 0x25 && b[1] === 0x50 && b[2] === 0x44 && b[3] === 0x46 },
  'image/jpeg': { ext: ['jpg', 'jpeg'], magic: (b) => b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff },
  'image/png': { ext: ['png'], magic: (b) => b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4e && b[3] === 0x47 },
}

/** Detect the real type from the first bytes; the browser's extension and MIME are never trusted. */
export function sniffFileType(head: Uint8Array): string | null {
  for (const [mime, def] of Object.entries(INTAKE_FILE_TYPES)) if (head.length >= 4 && def.magic(head)) return mime
  return null
}

export interface FileCheckInput { name: string; size: number; head: Uint8Array }
export type FileProblem = { file: string; problem: 'type' | 'too_large' | 'empty' }

export function validateIntakeFiles(files: FileCheckInput[], limits: IntakeLimits, policy: IntakePolicy): { ok: true; types: string[] } | { ok: false; problems: FileProblem[]; error: string } {
  const maxFiles = policy.max_files ?? limits.max_files
  const maxFile = (policy.max_file_mb ?? limits.max_file_mb) * 1024 * 1024
  const maxTotal = (policy.max_total_mb ?? limits.max_total_mb) * 1024 * 1024
  if (files.length > maxFiles) return { ok: false, problems: [], error: `Up to ${maxFiles} files per submission.` }
  const total = files.reduce((n, f) => n + f.size, 0)
  if (total > maxTotal) return { ok: false, problems: [], error: `Files total ${(total / 1024 / 1024).toFixed(1)} MB — the limit is ${maxTotal / 1024 / 1024} MB per submission.` }
  const problems: FileProblem[] = []
  const types: string[] = []
  for (const f of files) {
    if (f.size === 0) { problems.push({ file: f.name, problem: 'empty' }); continue }
    if (f.size > maxFile) { problems.push({ file: f.name, problem: 'too_large' }); continue }
    const t = sniffFileType(f.head)
    if (!t) { problems.push({ file: f.name, problem: 'type' }); continue }
    types.push(t)
  }
  if (problems.length) {
    const first = problems[0]
    const why = first.problem === 'type' ? 'is not a PDF, JPG or PNG' : first.problem === 'too_large' ? `is over ${maxFile / 1024 / 1024} MB` : 'is empty'
    return { ok: false, problems, error: `"${first.file}" ${why}.` }
  }
  return { ok: true, types }
}

/* ---------------- risk (§26) ---------------- */

const DISPOSABLE = new Set(['mailinator.com', 'guerrillamail.com', '10minutemail.com', 'tempmail.com', 'yopmail.com', 'trashmail.com', 'sharklasers.com', 'getnada.com', 'dispostable.com', 'temp-mail.org'])

export function isDisposableEmail(email: string): boolean {
  const d = email.split('@')[1]?.toLowerCase()
  return !!d && DISPOSABLE.has(d)
}

export interface RiskSignals {
  level: TrustLevel
  accountAgeHours: number | null
  userToday: number
  ipLastHour: number
  ipAccountsLastDay: number
  disposableEmail: boolean
  duplicateFiles: number
  botCheckFailed: boolean
  recentVerificationFailures: number
  honeypotFilled: boolean
}

/** 0–100. Signals add up; trust subtracts. Thresholds come from intake_settings. */
export function riskScore(s: RiskSignals): { score: number; reasons: string[] } {
  const reasons: string[] = []
  let score = 0
  const add = (n: number, why: string) => { score += n; reasons.push(why) }
  if (s.honeypotFilled) add(100, 'honeypot filled')
  if (s.botCheckFailed) add(60, 'bot check failed')
  if (s.level <= 1) add(25, 'email not verified')
  if (s.accountAgeHours !== null && s.accountAgeHours < 1) add(10, 'account under an hour old')
  if (s.userToday >= 5) add(10, 'high submission velocity')
  if (s.ipLastHour >= 10) add(15, 'many submissions from this IP')
  if (s.ipAccountsLastDay >= 3) add(20, 'several accounts created from this IP today')
  if (s.disposableEmail) add(20, 'disposable email domain')
  if (s.duplicateFiles > 0) add(15, 'repeated file')
  if (s.recentVerificationFailures >= 3) add(10, 'repeated failed verification')
  if (s.level >= 3) score -= 20
  if (s.level === 4) score -= 20
  return { score: Math.max(0, Math.min(100, score)), reasons }
}

export function riskDecision(score: number, limits: IntakeLimits): 'allow' | 'review' | 'block' {
  if (score >= limits.block_risk_score) return 'block'
  if (score >= limits.review_risk_score) return 'review'
  return 'allow'
}

/** What the person sees when a submission is refused. Never the internal rule name. */
export const DENIAL_MESSAGES: Record<IntakeDenial, string> = {
  auth_required: 'Verify your email to submit this trip.',
  blocked: 'Submissions from this account or network are blocked. Contact support if you believe this is a mistake.',
  intake_disabled: 'This intake page is not accepting submissions right now.',
  protection_mode: 'This intake page is temporarily accepting invited carriers only. Ask the broker for an invitation link, or try again later.',
  not_invited: 'This broker accepts submissions by invitation only. Ask them for an invitation link.',
  company_verification_required: 'This broker accepts submissions from verified carrier companies only. Verify your company under Settings first.',
  email_verification_required: 'Verify your email to submit this trip.',
  daily_quota: 'You have reached today\'s submission limit for this account. Verify your company for a higher limit, or try again tomorrow.',
  ip_velocity: 'Too many submissions from your network in the last hour. Please try again later.',
  page_velocity: 'This intake page is receiving a lot of submissions right now. Please try again in a little while.',
}
