import { and, eq, sql } from 'drizzle-orm';

import { db } from '@/lib/db';
import { interestBerths, interests } from '@/lib/db/schema/interests';
import { berths } from '@/lib/db/schema/berths';
import { systemSettings } from '@/lib/db/schema/system';
import { createAuditLog, type AuditMeta } from '@/lib/audit';
import { emitToRoom } from '@/lib/socket/server';
import { getPrimaryBerth } from '@/lib/services/interest-berths.service';
import { logger } from '@/lib/logger';

// ─── Types ────────────────────────────────────────────────────────────────────

export type BerthRuleTrigger =
  | 'eoi_sent'
  | 'eoi_signed'
  | 'reservation_signed'
  | 'deposit_received'
  | 'contract_signed'
  | 'interest_archived'
  | 'interest_completed'
  | 'deal_lost'
  | 'berth_unlinked';

export type BerthRuleMode = 'auto' | 'suggest' | 'off';

export interface BerthRuleResult {
  action: 'applied' | 'suggested' | 'none';
  newStatus?: string;
  message?: string;
}

interface RuleConfig {
  mode: BerthRuleMode;
  targetStatus: string;
}

// ─── Defaults ────────────────────────────────────────────────────────────────

const DEFAULT_RULES: Record<BerthRuleTrigger, RuleConfig> = {
  eoi_sent: { mode: 'suggest', targetStatus: 'under_offer' },
  eoi_signed: { mode: 'auto', targetStatus: 'under_offer' },
  // Reservation agreement signed — a commitment short of sale, so the berth
  // stays Under offer (audit H4); previously reused the contract_signed rule
  // and flipped it to Sold prematurely.
  reservation_signed: { mode: 'auto', targetStatus: 'under_offer' },
  deposit_received: { mode: 'auto', targetStatus: 'sold' },
  contract_signed: { mode: 'auto', targetStatus: 'sold' },
  interest_archived: { mode: 'suggest', targetStatus: 'available' },
  interest_completed: { mode: 'auto', targetStatus: 'sold' },
  // Fired when a deal is closed lost/cancelled (audit C2). Frees the berth
  // rather than the previous behaviour where ANY outcome reused
  // `interest_completed` and flipped the berth to 'sold'.
  deal_lost: { mode: 'auto', targetStatus: 'available' },
  berth_unlinked: { mode: 'off', targetStatus: 'available' },
};

// ─── Bundle-aware triggers (audit M4) ───────────────────────────────────────────
//
// A deal progressing (EOI sent/signed, reservation signed, deposit received,
// contract signed, won) commits the WHOLE EOI bundle, not just the primary
// berth. For these "status-advancing" triggers we flip every berth covered by
// the signature (`interest_berths.is_in_eoi_bundle = true`); otherwise a
// multi-berth bundle leaves its siblings on `available`/`under_offer` and they
// stay publicly visible + pitchable while the deal is locked up.
//
// The release/unlink triggers are deliberately NOT in this set:
//   • `interest_archived` / `deal_lost` target `available` and run in
//     suggest/auto-but-rarely-on modes; freeing the whole bundle is handled
//     elsewhere (smart-archive decision log) and isn't this trigger's job.
//   • `berth_unlinked` targets exactly one berth — the just-unlinked one
//     (audit M5) — via `targetBerthIdOverride`, never the bundle.
const BUNDLE_TRIGGERS: ReadonlySet<BerthRuleTrigger> = new Set<BerthRuleTrigger>([
  'eoi_sent',
  'eoi_signed',
  'reservation_signed',
  'deposit_received',
  'contract_signed',
  'interest_completed',
]);

// ─── Config ───────────────────────────────────────────────────────────────────

async function getRulesConfig(portId: string): Promise<Record<BerthRuleTrigger, RuleConfig>> {
  const setting = await db.query.systemSettings.findFirst({
    where: and(eq(systemSettings.key, 'berth_rules'), eq(systemSettings.portId, portId)),
  });

  if (!setting?.value) {
    return { ...DEFAULT_RULES };
  }

  const stored = setting.value as Partial<Record<BerthRuleTrigger, RuleConfig>>;
  const merged = { ...DEFAULT_RULES };

  for (const trigger of Object.keys(DEFAULT_RULES) as BerthRuleTrigger[]) {
    if (stored[trigger]) {
      merged[trigger] = stored[trigger]!;
    }
  }

  return merged;
}

// ─── Evaluate Rule ────────────────────────────────────────────────────────────

export async function evaluateRule(
  trigger: BerthRuleTrigger,
  interestId: string,
  portId: string,
  meta: AuditMeta,
  /**
   * Force the rule onto a specific berth instead of resolving the target from
   * the interest's primary/bundle. Required by `berth_unlinked` (audit M5):
   * the junction row is deleted before the rule fires, so resolving via
   * `getPrimaryBerth` would target a DIFFERENT still-linked berth. The caller
   * passes the just-unlinked berthId here and evaluates BEFORE the delete.
   */
  targetBerthIdOverride?: string,
): Promise<BerthRuleResult> {
  const interest = await db.query.interests.findFirst({
    where: and(eq(interests.id, interestId), eq(interests.portId, portId)),
  });

  if (!interest) {
    return { action: 'none' };
  }

  // Resolve which berth(s) this rule targets:
  //   • explicit override (berth_unlinked) → exactly that berth;
  //   • status-advancing bundle trigger → every berth covered by the EOI
  //     signature (`is_in_eoi_bundle = true`) so siblings don't go stale
  //     (audit M4);
  //   • everything else → the interest's primary berth (plan §3.4), resolved
  //     via interest_berths rather than the legacy column.
  let targetBerthIds: string[];
  if (targetBerthIdOverride) {
    targetBerthIds = [targetBerthIdOverride];
  } else if (BUNDLE_TRIGGERS.has(trigger)) {
    const bundleRows = await db
      .select({ berthId: interestBerths.berthId })
      .from(interestBerths)
      .where(
        and(eq(interestBerths.interestId, interestId), eq(interestBerths.isInEoiBundle, true)),
      );
    targetBerthIds = bundleRows.map((r) => r.berthId);
    if (targetBerthIds.length === 0) {
      // No bundle rows (e.g. a single primary that somehow lost its bundle
      // flag, or a berthless interest). Fall back to the primary so the
      // common single-berth case still advances.
      const primaryBerth = await getPrimaryBerth(interestId);
      if (primaryBerth?.berthId) targetBerthIds = [primaryBerth.berthId];
    }
  } else {
    const primaryBerth = await getPrimaryBerth(interestId);
    targetBerthIds = primaryBerth?.berthId ? [primaryBerth.berthId] : [];
  }

  if (targetBerthIds.length === 0) {
    return { action: 'none' };
  }

  const rulesConfig = await getRulesConfig(portId);
  const rule = rulesConfig[trigger];

  for (const targetBerthId of targetBerthIds) {
    await applyRuleToBerth(trigger, rule, interestId, portId, targetBerthId, meta);
  }

  if (rule.mode === 'off') {
    return { action: 'none' };
  }
  if (rule.mode === 'auto') {
    // Preserve the original contract: auto mode reports 'applied' (with the
    // rule's target status) regardless of whether any individual berth was a
    // no-op idempotent re-fire.
    return { action: 'applied', newStatus: rule.targetStatus };
  }

  // suggest mode - the decision-trace audit already records the suggestion.
  return {
    action: 'suggested',
    newStatus: rule.targetStatus,
    message: `Suggested status change to "${rule.targetStatus}" based on trigger "${trigger}"`,
  };
}

/**
 * Apply a resolved rule to a single berth under the per-berth advisory lock +
 * idempotency pattern. Factored out of {@link evaluateRule} so the multi-berth
 * bundle path (audit M4) reuses the exact same locking/auditing/socket emit
 * for every berth it touches.
 */
async function applyRuleToBerth(
  trigger: BerthRuleTrigger,
  rule: RuleConfig,
  interestId: string,
  portId: string,
  targetBerthId: string,
  meta: AuditMeta,
): Promise<void> {
  // Decision-trace audit: ALWAYS record what we decided to do (or not do),
  // including the rule mode, so admins can debug "why didn't this fire?" /
  // "why did this fire" without grepping server logs. Tagged `berth_rule_decision`
  // so it's distinct from the actual mutation audit row below.
  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'rule_evaluated',
    entityType: 'berth',
    entityId: targetBerthId,
    metadata: {
      type: 'berth_rule_decision',
      trigger,
      mode: rule.mode,
      targetStatus: rule.targetStatus,
      interestId,
    },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  if (rule.mode === 'off') {
    return;
  }

  if (rule.mode === 'auto') {
    // Concurrency hardening: wrap the read-then-write in a transaction with a
    // berth-scoped advisory lock so two concurrent webhook retries can't both
    // commit the same status flip (which produces duplicate audit rows + a
    // double socket emit). Also short-circuit when the target status is
    // already in place - re-writing 'sold'→'sold' is technically harmless
    // but pollutes the audit trail and the socket stream.
    const result = await db.transaction(async (tx) => {
      // pg_advisory_xact_lock takes a single bigint. We hash port+berth into
      // a stable 32-bit slot. The lock auto-releases at transaction end so
      // there's no risk of a stuck lock if the handler crashes mid-write.
      await tx.execute(
        sql`SELECT pg_advisory_xact_lock(hashtext(${`berth-rule:${portId}:${targetBerthId}`}))`,
      );

      // Re-read inside the lock so we observe the post-lock state, not the
      // pre-lock snapshot. If the prior contender already moved status to
      // our target, we're idempotent and bail.
      const [current] = await tx
        .select({ status: berths.status, statusOverrideMode: berths.statusOverrideMode })
        .from(berths)
        .where(and(eq(berths.id, targetBerthId), eq(berths.portId, portId)));

      if (!current) return { changed: false as const };

      // Soft-pin priority: a human-set manual override wins over automatic
      // rules EXCEPT a real sale (target 'sold' — deposit_received /
      // contract_signed / interest_completed). Soft triggers (eoi_*,
      // reservation→under_offer, *→available) must not stomp a manual pin;
      // the pin persists until a human changes or resets it. Sale triggers
      // still override because a confirmed sale is a fact a stale pin can't
      // be allowed to hide.
      if (current.statusOverrideMode === 'manual' && rule.targetStatus !== 'sold') {
        logger.debug(
          {
            trigger,
            targetBerthId,
            portId,
            status: current.status,
            targetStatus: rule.targetStatus,
          },
          'Berth-rule auto: respecting manual pin, skipping non-sale auto-write',
        );
        return { changed: false as const };
      }

      if (current.status === rule.targetStatus) {
        // Idempotent re-fire. We already audited the decision above; nothing
        // more to do here.
        logger.debug(
          { trigger, targetBerthId, portId, status: current.status },
          'Berth-rule auto: target status already set, skipping duplicate write',
        );
        return { changed: false as const };
      }

      await tx
        .update(berths)
        .set({
          status: rule.targetStatus,
          statusLastChangedBy: meta.userId,
          statusLastChangedReason: `Auto-applied by rule: ${trigger}`,
          statusLastModified: new Date(),
          // #67 Phase 1: stamp the source so the reconciliation queue
          // can filter "Manual only" - rules-engine writes are never
          // candidates for catch-up because they already have a backing
          // interest driving them.
          statusOverrideMode: 'automated',
          updatedAt: new Date(),
        })
        .where(and(eq(berths.id, targetBerthId), eq(berths.portId, portId)));

      return { changed: true as const, previousStatus: current.status };
    });

    if (result.changed) {
      void createAuditLog({
        userId: meta.userId,
        portId,
        action: 'update',
        entityType: 'berth',
        entityId: targetBerthId,
        oldValue: { status: result.previousStatus },
        newValue: { status: rule.targetStatus },
        metadata: { type: 'berth_rule_auto', trigger, interestId },
        ipAddress: meta.ipAddress,
        userAgent: meta.userAgent,
      });

      emitToRoom(`port:${portId}`, 'berth:statusChanged', {
        berthId: targetBerthId,
        newStatus: rule.targetStatus,
        triggeredBy: meta.userId,
        trigger,
      });
    }

    return;
  }

  // suggest mode - the decision-trace audit above already records the suggestion.
}
