import 'server-only'

import { createAdminClient } from '@/lib/supabase/admin'
import { logTripEvent } from '@/lib/audit'
import { logOperationEvent } from '@/lib/observability/operation-events'
import { sendPlatformEmail } from '@/lib/email/send'
import { SITE_URL } from '@/lib/app-url'
import { emailPattern } from '@/lib/like'
import { isMissingColumn } from '@/lib/db-compat'
import { loadUserContexts } from '@/lib/data/modes'
import { logAutoParticipantRuleEvent } from '@/lib/data/auto-participant-events'
import { findContext, type UserContext } from '@/lib/domain/modes'
import {
  autoAddDecision, companyScopeMatches, contextChoice, initialTripRoleForCompanyMember, isAutoAdded, tripRoleForRoleType,
  type AutoAddSkipReason,
} from '@/lib/domain/auto-participants'
import { PARTICIPANT_AUTO_ADDED_ACTION, describePilotAccess, inheritedPilotAccess, pilotAccessFromEvents } from '@/lib/domain/pilot'
import type { AutoParticipantRule, Trip, TripParticipant, TripRole } from '@/types/db'

/**
 * The central Auto-Participant resolver (§29, 2026-09-30). ONE place decides
 * who gets added when a person is placed on a trip; every entry point calls
 * it (trip creation, invites, acceptance, intake, sign-in linking, Synchron
 * import, claims, the per-trip context chooser) and none of them carries the
 * rules themselves.
 *
 * Never recursive (§5): a row this resolver created never triggers again.
 * Fire-once (D12): a row is evaluated the first time it is placed or
 * activated; `rules_evaluated_at` remembers that.
 */

type Admin = ReturnType<typeof createAdminClient>

export type TriggerKind = 'participation' | 'claim' | 'synchron_import' | 'context_chosen'

export interface ResolverSession {
  id: string
  originId: string
  preview?: string | null
  contextKey?: string | null
}

export interface ResolveParams {
  tripId: string
  /** The row that was just placed on the trip or activated. */
  participant: TripParticipant
  trigger: TriggerKind
  /** The session behind the action, when there is one (none for imports and claims). */
  session?: ResolverSession | null
  admin?: Admin
}

export type RuleOutcome = { ruleId: string; outcome: 'added' | 'already_present' | 'skipped'; reason?: AutoAddSkipReason; participantId?: string }

export interface ResolveResult {
  outcome: 'evaluated' | 'context_pending' | 'not_applicable' | 'already_evaluated'
  added: string[]
  rules: RuleOutcome[]
}

const MISSING = (e: { message?: string; code?: string } | null | undefined) =>
  !!e && (e.code === '42P01' || e.code === 'PGRST205' || isMissingColumn(e, 'addition_method', 'context_role_type', 'rules_evaluated_at') || /does not exist|schema cache/i.test(e.message ?? ''))

/* ------------------------------------------------------------ context */

export interface ContextResolution {
  status: 'set' | 'pending' | 'none'
  participant: TripParticipant
  /** Offered when several contexts match (the chooser). */
  choices?: UserContext[]
  contexts: UserContext[]
}

async function accountRoleFor(userId: string, admin: Admin): Promise<string> {
  const { data } = await admin.from('profiles').select('default_role').eq('id', userId).maybeSingle()
  return ((data as { default_role?: string } | null)?.default_role ?? 'dispatcher') as string
}

/**
 * Decision D13: which company and role does this person act with on this
 * trip? Stored on the row once known. The creator's choice is the active
 * mode; everyone else with exactly one matching context gets it silently;
 * several → the chooser (status `pending`); none → nothing to choose.
 */
export async function ensureParticipantContext(
  participant: TripParticipant,
  session: ResolverSession | null | undefined,
  admin: Admin = createAdminClient(),
): Promise<ContextResolution> {
  if (!participant.user_id) return { status: 'none', participant, contexts: [] }
  const contexts = await loadUserContexts({ id: participant.user_id, role: await accountRoleFor(participant.user_id, admin) }, admin)
  const active = contexts.filter((c) => c.status === 'active')

  const store = async (c: UserContext): Promise<ContextResolution> => {
    const patch: Partial<TripParticipant> = { context_company_id: c.companyId, context_role_type: c.roleType }
    const { error } = await admin.from('trip_participants').update(patch).eq('id', participant.id)
    if (error && !MISSING(error)) console.error('participant context store failed', error.message)
    return { status: 'set', participant: { ...participant, ...patch }, contexts }
  }

  // Already complete: role known and either a company is known or the role has no company behind it.
  if (participant.context_role_type) {
    if (participant.context_company_id) return { status: 'set', participant, contexts }
    const sameRole = active.filter((c) => c.roleType === participant.context_role_type)
    if (sameRole.length === 0) return { status: 'set', participant, contexts }
    if (sameRole.length === 1) return store(sameRole[0])
    return { status: 'pending', participant, choices: sameRole, contexts }
  }

  // A company rule knows the company but not the role (A4): choose among the roles held there.
  if (participant.context_company_id) {
    const pick = contextChoice(active, participant.role, participant.context_company_id)
    if (pick.kind === 'one') return store(pick.context)
    if (pick.kind === 'choose') return { status: 'pending', participant, choices: pick.contexts, contexts }
    return { status: 'none', participant, contexts }
  }

  // The creator acts in the mode they are in (D13).
  if (session && session.id === participant.user_id && session.contextKey) {
    const current = findContext(active, session.contextKey)
    if (current && tripRoleForRoleType(current.roleType) === participant.role) return store(current)
  }

  const pick = contextChoice(active, participant.role)
  if (pick.kind === 'one') return store(pick.context)
  if (pick.kind === 'choose') return { status: 'pending', participant, choices: pick.contexts, contexts }
  return { status: 'none', participant, contexts }
}

/* ----------------------------------------------------------- resolver */

export async function resolveAutoParticipantsForUserParticipation(params: ResolveParams): Promise<ResolveResult> {
  const admin = params.admin ?? createAdminClient()
  const row = params.participant
  const empty = (outcome: ResolveResult['outcome']): ResolveResult => ({ outcome, added: [], rules: [] })

  if (!row.user_id) return empty('not_applicable')
  if (row.status !== 'invited' && row.status !== 'active') return empty('not_applicable')
  if (isAutoAdded(row)) return empty('not_applicable') // §5 — never recursive
  if (row.rules_evaluated_at) return empty('already_evaluated') // D12 — fire once
  if (params.session && (params.session.originId !== params.session.id || params.session.preview)) return empty('not_applicable') // §39

  const ctx = await ensureParticipantContext(row, params.session, admin)
  if (ctx.status === 'pending') return empty('context_pending')
  const participant = ctx.participant

  const { data: trip } = await admin.from('trips').select('id, ref_code, origin, destination, created_at, imported_at, carrier_name').eq('id', params.tripId).maybeSingle()
  if (!trip) return empty('not_applicable')
  const tripRow = trip as Pick<Trip, 'id' | 'ref_code' | 'origin' | 'destination' | 'created_at'> & { imported_at?: string | null; carrier_name?: string | null }
  // D10: a claim fires only for trips imported on or after the rule.
  const at = params.trigger === 'claim' ? (tripRow.imported_at ?? tripRow.created_at) : new Date().toISOString()

  // Personal rules follow the person; company rules follow the company they act for here (D2, D13).
  const personal = await admin.from('auto_participant_rules').select('*').eq('rule_type', 'personal').eq('owner_user_id', participant.user_id).eq('status', 'active')
  if (personal.error && MISSING(personal.error)) return empty('not_applicable')
  let rules = ((personal.data ?? []) as AutoParticipantRule[])
  if (participant.context_company_id && participant.context_role_type) {
    const holds = ctx.contexts.some((c) => c.status === 'active' && c.companyId === participant.context_company_id && c.roleType === participant.context_role_type)
    if (holds) {
      const company = await admin.from('auto_participant_rules').select('*').eq('rule_type', 'company').eq('owner_company_id', participant.context_company_id).eq('status', 'active')
      rules = rules.concat(((company.data ?? []) as AutoParticipantRule[]).filter((r) => companyScopeMatches(r.scope_type, participant.context_role_type)))
    }
  }

  const result: ResolveResult = { outcome: 'evaluated', added: [], rules: [] }
  for (const rule of rules) {
    try {
      const r = await addAutoParticipant({ rule, trip: tripRow, trigger: participant, triggerKind: params.trigger, at, session: params.session ?? null, admin })
      result.rules.push(r)
      if (r.outcome === 'added' && r.participantId) result.added.push(r.participantId)
    } catch (error) {
      console.error('auto-participant rule failed', { rule: rule.id, error: error instanceof Error ? error.message : error })
      await logOperationEvent({ area: 'system', event: 'auto_participant_rule_failed', outcome: 'failed', tripId: params.tripId, detail: { rule_id: rule.id } })
    }
  }
  const { error } = await admin.from('trip_participants').update({ rules_evaluated_at: new Date().toISOString() }).eq('id', participant.id)
  if (error && !MISSING(error)) console.error('rules_evaluated_at update failed', error.message)
  return result
}

/**
 * The call every entry point makes. Swallows every failure: a resolver
 * problem must never fail the trip creation, invite or import that caused it.
 */
export async function evaluateParticipantRules(params: ResolveParams): Promise<ResolveResult | null> {
  try {
    return await resolveAutoParticipantsForUserParticipation(params)
  } catch (error) {
    console.error('auto-participant evaluation failed', { trip: params.tripId, error: error instanceof Error ? error.message : error })
    await logOperationEvent({ area: 'system', event: 'auto_participant_evaluation_failed', outcome: 'failed', tripId: params.tripId, detail: { participant_id: params.participant.id } })
    return null
  }
}

/** Load a row by id and evaluate it — for callers that only hold the id. */
export async function evaluateParticipantRulesById(params: { tripId: string; participantId: string; trigger: TriggerKind; session?: ResolverSession | null; admin?: Admin }): Promise<ResolveResult | null> {
  const admin = params.admin ?? createAdminClient()
  const { data } = await admin.from('trip_participants').select('*').eq('id', params.participantId).maybeSingle()
  if (!data) return null
  return evaluateParticipantRules({ tripId: params.tripId, participant: data as TripParticipant, trigger: params.trigger, session: params.session, admin })
}

/**
 * Every current, directly placed participant of a trip — tests and a future
 * explicit backfill only (§30). No page or job calls this.
 */
export async function resolveAutoParticipantsForTrip(tripId: string, admin: Admin = createAdminClient()): Promise<ResolveResult[]> {
  const { data } = await admin.from('trip_participants').select('*').eq('trip_id', tripId).in('status', ['invited', 'active'])
  const out: ResolveResult[] = []
  for (const row of (data ?? []) as TripParticipant[]) {
    if (isAutoAdded(row)) continue
    out.push(await resolveAutoParticipantsForUserParticipation({ tripId, participant: row, trigger: 'participation', session: null, admin }))
  }
  return out
}

/* ---------------------------------------------------------- one rule */

interface AddParams {
  rule: AutoParticipantRule
  trip: Pick<Trip, 'id' | 'ref_code' | 'origin' | 'destination'> & { carrier_name?: string | null }
  trigger: TripParticipant
  triggerKind: TriggerKind
  at: string
  session: ResolverSession | null
  admin: Admin
}

async function existingRowFor(admin: Admin, tripId: string, userId: string, email: string): Promise<TripParticipant | null> {
  const byUser = await admin.from('trip_participants').select('*').eq('trip_id', tripId).eq('user_id', userId).limit(1)
  const first = ((byUser.data ?? []) as TripParticipant[])[0]
  if (first) return first
  const byEmail = await admin.from('trip_participants').select('*').eq('trip_id', tripId).ilike('email', emailPattern(email)).limit(1)
  return ((byEmail.data ?? []) as TripParticipant[])[0] ?? null
}

/** Task 5.2: add ONE person for ONE rule, or record why not. */
export async function addAutoParticipant(p: AddParams): Promise<RuleOutcome> {
  const { rule, trip, trigger, admin } = p
  const skip = async (reason: AutoAddSkipReason, extra: Record<string, unknown> = {}): Promise<RuleOutcome> => {
    await logAutoParticipantRuleEvent({ ruleId: rule.id, event: 'participant_skipped', tripId: trip.id, detail: { reason, trigger_participant: trigger.id, ...extra } })
    return { ruleId: rule.id, outcome: 'skipped', reason }
  }

  // The person the rule names.
  const personId = rule.participant_user_id
  const { data: profile } = personId
    ? await admin.from('profiles').select('id, email, full_name, claim_status, default_role').eq('id', personId).maybeSingle()
    : { data: null }
  const person = profile as { id: string; email: string; full_name: string | null; claim_status?: string | null; default_role?: string | null } | null
  const { data: account } = person
    ? await admin.from('auth_accounts').select('user_id, blocked_at, email_verified_at').eq('user_id', person.id).maybeSingle()
    : { data: null }
  const acct = account as { blocked_at: string | null; email_verified_at: string | null } | null

  // Company rules: still a member (or accepted as external)?
  let membershipOk: boolean | undefined
  let rolesAtCompany: string[] = []
  if (rule.rule_type === 'company' && person && rule.owner_company_id) {
    const { data: m } = await admin.from('company_memberships').select('id, status').eq('user_id', person.id).eq('company_id', rule.owner_company_id).maybeSingle()
    const approved = (m as { id: string; status: string } | null)?.status === 'approved'
    if (approved) {
      const { data: roles } = await admin.from('membership_roles').select('role_type, status').eq('user_id', person.id).eq('company_id', rule.owner_company_id).eq('status', 'active')
      rolesAtCompany = ((roles ?? []) as { role_type: string }[]).map((r) => r.role_type)
    }
    membershipOk = approved || (rule.requires_acceptance && !!rule.accepted_at)
  }

  const existing = person ? await existingRowFor(admin, trip.id, person.id, person.email) : null
  const decision = autoAddDecision({
    rule,
    trigger: { participant: trigger, kind: p.triggerKind, at: p.at, session: p.session },
    participant: {
      userId: person?.id ?? null,
      blockedAt: acct?.blocked_at ?? null,
      claimStatus: person?.claim_status ?? null,
      // A profile the app created (claim_status null) or a claimed one is a real user; env logins may have no auth_accounts row.
      hasAccount: !!person,
      membershipOk,
      existingStatus: existing?.status ?? null,
    },
  })
  if (!decision.add) {
    if (decision.reason === 'already_on_trip' && existing) {
      await admin.from('trip_participant_sources').insert({ participant_id: existing.id, rule_id: rule.id, triggering_user_id: trigger.user_id, triggering_company_id: rule.owner_company_id })
      await logAutoParticipantRuleEvent({ ruleId: rule.id, event: 'participant_already_present', tripId: trip.id, participantId: existing.id, detail: { trigger_participant: trigger.id } })
      return { ruleId: rule.id, outcome: 'already_present', participantId: existing.id }
    }
    return skip(decision.reason)
  }

  // The role they take part with: their own (D1); a pilot trigger makes them a pilot with the same limits (D14).
  let role: TripRole | null
  if (trigger.role === 'pilot') role = 'pilot'
  else if (rule.participant_role_type) role = tripRoleForRoleType(rule.participant_role_type)
  else if (rule.rule_type === 'company') role = initialTripRoleForCompanyMember(rolesAtCompany)
  else role = tripRoleForRoleType(accountRoleType(person!.default_role))
  if (!role) return skip('participant_unresolved', { why: 'no_role' })

  // Context on the trip (D13 / A10): a company rule names the company; a personal rule leaves the company to the person.
  const context = rule.rule_type === 'company'
    ? { context_company_id: rule.owner_company_id, context_role_type: rule.participant_role_type }
    : { context_company_id: null, context_role_type: rule.participant_role_type }

  const insert = {
    trip_id: trip.id,
    user_id: person!.id,
    email: person!.email,
    name: person!.full_name || person!.email,
    role,
    status: 'active',
    invited_by: trigger.user_id,
    addition_method: rule.rule_type === 'company' ? 'company_auto_participant' : 'personal_auto_participant',
    triggering_user_id: trigger.user_id,
    triggering_company_id: rule.rule_type === 'company' ? rule.owner_company_id : null,
    auto_participant_rule_id: rule.id,
    ...context,
  }
  const { data: created, error } = await admin.from('trip_participants').insert(insert).select('*').single()
  if (error) {
    if (error.code === '23505' || /duplicate/i.test(error.message)) {
      const dup = await existingRowFor(admin, trip.id, person!.id, person!.email)
      if (dup) {
        await admin.from('trip_participant_sources').insert({ participant_id: dup.id, rule_id: rule.id, triggering_user_id: trigger.user_id, triggering_company_id: rule.owner_company_id })
        return { ruleId: rule.id, outcome: 'already_present', participantId: dup.id }
      }
    }
    throw new Error(`auto participant insert: ${error.message}`)
  }
  const row = created as TripParticipant
  await admin.from('trip_participant_sources').insert({ participant_id: row.id, rule_id: rule.id, triggering_user_id: trigger.user_id, triggering_company_id: rule.owner_company_id })

  // D1/D14: a pilot can never hand out more than they hold.
  let pilotAccess: string | null = null
  if (trigger.role === 'pilot') {
    const { data: events } = await admin.from('trip_events').select('action, detail').eq('trip_id', trip.id).in('action', ['participant_invited', 'pilot_access_shared', PARTICIPANT_AUTO_ADDED_ACTION]).order('created_at')
    const held = pilotAccessFromEvents((events ?? []) as { action: string; detail: Record<string, unknown> | null }[])[trigger.email.toLowerCase()]
    pilotAccess = held ?? describePilotAccess(inheritedPilotAccess(null))
  }

  const triggerName = trigger.name || trigger.email
  let companyName: string | null = null
  if (rule.owner_company_id) {
    const { data: c } = await admin.from('companies').select('display_name, legal_name').eq('id', rule.owner_company_id).maybeSingle()
    companyName = (c as { display_name?: string; legal_name?: string } | null)?.display_name || (c as { legal_name?: string } | null)?.legal_name || null
  }

  await logTripEvent({
    tripId: trip.id,
    actorLabel: 'system',
    action: PARTICIPANT_AUTO_ADDED_ACTION,
    detail: {
      email: row.email, role: row.role, participant_id: row.id, rule_type: rule.rule_type, rule_id: rule.id,
      triggering_user_id: trigger.user_id, triggering_user: triggerName, triggering_company_id: rule.owner_company_id, company_name: companyName,
      ...(pilotAccess ? { pilot_access: pilotAccess } : {}),
    },
  })
  await logAutoParticipantRuleEvent({ ruleId: rule.id, event: 'participant_added', tripId: trip.id, participantId: row.id, detail: { role: row.role, trigger_participant: trigger.id, trigger_kind: p.triggerKind } })

  // D8: one admin-editable "you were automatically added" email (preferences apply, §24).
  const origin = process.env.NEXT_PUBLIC_APP_URL?.trim().replace(/\/+$/, '') || SITE_URL
  try {
    await sendPlatformEmail({
      templateKey: 'auto_participant_added',
      to: row.email,
      userId: row.user_id,
      origin,
      data: {
        first_name: (person!.full_name || '').trim().split(/\s+/)[0] || person!.email,
        trip_id: trip.ref_code,
        trip_route: [trip.origin, trip.destination].filter(Boolean).join(' → '),
        rule_type: rule.rule_type === 'company' ? 'company' : 'personal',
        triggering_user: triggerName,
        company_name: companyName ?? '',
        reason: rule.rule_type === 'company'
          ? `under ${companyName ?? 'your company'}'s company Auto-Participant policy`
          : `because you are configured as an Auto-Participant for ${triggerName}`,
        workspace_link: `${origin}/trips/${trip.id}`,
      },
    })
  } catch (error) {
    console.error('auto_participant_added email failed', error instanceof Error ? error.message : error)
  }

  return { ruleId: rule.id, outcome: 'added', participantId: row.id }
}

function accountRoleType(accountRole: string | null | undefined): string | null {
  if (accountRole === 'dispatcher') return 'carrier_dispatcher'
  if (accountRole === 'driver') return 'carrier_driver'
  if (accountRole === 'broker') return 'freight_broker'
  return null
}
