/**
 * Auto-promote a captured website berth registration into a prospect.
 *
 * The marketing website dual-writes every inquiry into `website_submissions`
 * (capture-only - see `/api/public/website-inquiries`). By default those sit
 * in the triage inbox until a rep promotes them, and promotion is what marks
 * the berth "Under Offer" on the public map (Option 1).
 *
 * When the per-port flag `website_berth_autopromote_enabled` is ON (Option 2),
 * a registration for a SPECIFIC, currently-available berth skips the wait: we
 * create the prospect immediately (client deduped by email + optional yacht +
 * interest) and link the berth with `is_specific_interest=true`, which
 * `derivePublicStatus` reads as "Under Offer" - so the public map flips the
 * moment the registration lands. The submission is marked `converted` so a rep
 * never double-creates it.
 *
 * Safety posture:
 *   - Only fires for `berth_inquiry` submissions that name a resolvable berth
 *     whose base status is exactly `available`. Sold / Under Offer / unknown
 *     berths are left alone (never stomp a manual or sold state).
 *   - Skips when the same client already has an open specific-interest link on
 *     that berth (double-submit guard).
 *   - Default OFF; intended to be flipped on only post-cutover so it never
 *     races the NocoDB-primary migration window.
 *   - Caller invokes this fire-and-forget after the idempotent capture insert,
 *     so a failure here can never 500 the public capture POST.
 */

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

import { db } from '@/lib/db';
import { withTransaction } from '@/lib/db/utils';
import { systemSettings } from '@/lib/db/schema/system';
import { berths } from '@/lib/db/schema/berths';
import { interests, interestBerths } from '@/lib/db/schema/interests';
import { clients, clientContacts } from '@/lib/db/schema/clients';
import { yachts, yachtOwnershipHistory } from '@/lib/db/schema/yachts';
import { websiteSubmissions } from '@/lib/db/schema/website-submissions';
import { upsertInterestBerthTx } from '@/lib/services/interest-berths.service';
import { extractInquiryFields } from '@/lib/services/website-intake-fields';

/** Per-port gate. Default OFF (no row -> disabled). */
export async function isWebsiteBerthAutopromoteEnabled(portId: string): Promise<boolean> {
  const row = await db
    .select({ value: systemSettings.value })
    .from(systemSettings)
    .where(
      and(
        eq(systemSettings.key, 'website_berth_autopromote_enabled'),
        or(eq(systemSettings.portId, portId), isNull(systemSettings.portId)),
      ),
    )
    .limit(1);
  return row[0]?.value === true;
}

/**
 * Pure decision helper: does this submission express interest in a SPECIFIC
 * berth? Only `berth_inquiry` submissions that carry a non-empty mooring
 * number qualify. Kept pure + exported for unit testing.
 */
export function extractPromotionIntent(
  kind: string,
  payload: Record<string, unknown>,
): { hasSpecificBerth: boolean; mooringNumber: string | null } {
  if (kind !== 'berth_inquiry') return { hasSpecificBerth: false, mooringNumber: null };
  const fields = extractInquiryFields(payload);
  return {
    hasSpecificBerth: !!fields.mooringNumber,
    mooringNumber: fields.mooringNumber,
  };
}

/**
 * Pure helper: a berth is auto-promotable only when its base status is exactly
 * `available`. Anything else (under_offer / sold / unknown) is left untouched.
 */
export function isBerthPromotable(status: string | null | undefined): boolean {
  return status === 'available';
}

/** Strip the website's appended "ft" unit and return a numeric string, or null. */
function parseFeet(value: unknown): string | null {
  if (typeof value !== 'string' && typeof value !== 'number') return null;
  const m = String(value).match(/-?\d+(?:\.\d+)?/);
  return m ? m[0] : null;
}

export interface AutoPromoteInput {
  portId: string;
  submissionId: string;
  kind: string;
  payload: Record<string, unknown>;
}

export type AutoPromoteResult =
  | { promoted: false; reason: string }
  | {
      promoted: true;
      interestId: string;
      clientId: string;
      berthId: string;
      mooringNumber: string;
    };

/**
 * Attempt to auto-promote a captured berth submission. Returns a structured
 * result describing what happened (or why it was skipped). Never throws for
 * "skip" conditions - only genuinely unexpected DB errors propagate, and the
 * caller wraps this in `.catch`.
 */
export async function autoPromoteWebsiteBerthInquiry(
  input: AutoPromoteInput,
): Promise<AutoPromoteResult> {
  const { portId, submissionId, kind, payload } = input;

  const intent = extractPromotionIntent(kind, payload);
  if (!intent.hasSpecificBerth || !intent.mooringNumber) {
    return { promoted: false, reason: 'no-specific-berth' };
  }
  const mooringNumber = intent.mooringNumber;

  // Resolve the berth (read-only, outside the tx).
  const berth = await db.query.berths.findFirst({
    where: and(eq(berths.mooringNumber, mooringNumber), eq(berths.portId, portId)),
    columns: { id: true, status: true },
  });
  if (!berth) {
    return { promoted: false, reason: 'berth-not-found' };
  }
  if (!isBerthPromotable(berth.status)) {
    return { promoted: false, reason: `berth-not-available:${berth.status}` };
  }
  const berthId = berth.id;

  const fields = extractInquiryFields(payload);
  if (!fields.email) {
    // No email -> we can't dedup or contact; leave it in triage for a human.
    return { promoted: false, reason: 'no-email' };
  }
  const normalizedEmail = fields.email.trim().toLowerCase();
  const fullName = fields.fullName || 'Unknown';

  const yachtName =
    typeof payload.berth_yacht_name === 'string' ? payload.berth_yacht_name.trim() : '';
  const lengthFt = parseFeet(payload.berth_min_length);
  const widthFt = parseFeet(payload.berth_min_width);
  const draftFt = parseFeet(payload.berth_min_draught);

  const result = await withTransaction(async (tx) => {
    // 1. Find-or-create client by lowercased primary email (mirrors the eager
    //    public-interest dedup so a known registrant doesn't fork a new row).
    let clientId: string;
    const existingContact = await tx.query.clientContacts.findFirst({
      where: and(
        eq(clientContacts.channel, 'email'),
        sql`LOWER(${clientContacts.value}) = ${normalizedEmail}`,
      ),
    });
    if (existingContact) {
      const existingClient = await tx.query.clients.findFirst({
        where: eq(clients.id, existingContact.clientId),
      });
      if (existingClient && existingClient.portId === portId) {
        clientId = existingClient.id;
      } else {
        clientId = await createPromotedClient(tx, portId, fullName, normalizedEmail, fields.phone);
      }
    } else {
      clientId = await createPromotedClient(tx, portId, fullName, normalizedEmail, fields.phone);
    }

    // 2. Double-submit guard: bail if this client already has an OPEN interest
    //    with a specific-interest link on this berth.
    const existingLink = await tx
      .select({ id: interests.id })
      .from(interests)
      .innerJoin(interestBerths, eq(interestBerths.interestId, interests.id))
      .where(
        and(
          eq(interests.clientId, clientId),
          eq(interests.portId, portId),
          isNull(interests.archivedAt),
          isNull(interests.outcome),
          eq(interestBerths.berthId, berthId),
          eq(interestBerths.isSpecificInterest, true),
        ),
      )
      .limit(1);
    if (existingLink[0]) {
      return { kind: 'skip' as const, reason: 'duplicate-open-interest' };
    }

    // 3. Optional yacht (only when the registrant named one). Owner = client.
    let yachtId: string | null = null;
    if (yachtName) {
      const [newYacht] = await tx
        .insert(yachts)
        .values({
          portId,
          name: yachtName,
          lengthFt,
          widthFt,
          draftFt,
          currentOwnerType: 'client',
          currentOwnerId: clientId,
          status: 'active',
        })
        .returning();
      yachtId = newYacht!.id;
      await tx.insert(yachtOwnershipHistory).values({
        yachtId,
        ownerType: 'client',
        ownerId: clientId,
        startDate: new Date(),
        endDate: null,
        createdBy: 'website-auto-promote',
      });
    }

    // 4. Create the interest.
    const [newInterest] = await tx
      .insert(interests)
      .values({
        portId,
        clientId,
        yachtId,
        source: 'website',
        pipelineStage: 'enquiry',
      })
      .returning();
    const interestId = newInterest!.id;

    // 5. Link the berth via the canonical junction helper. is_specific_interest
    //    true => the public map derives "Under Offer". isPrimary forces the
    //    in-bundle invariant (matches the eager public-interest path).
    await upsertInterestBerthTx(tx, interestId, berthId, {
      isPrimary: true,
      isSpecificInterest: true,
      addedBy: 'website-auto-promote',
    });

    // 6. Mark the captured submission converted so triage never double-creates.
    await tx
      .update(websiteSubmissions)
      .set({
        triageState: 'converted',
        triagedAt: new Date(),
        triagedBy: 'system:website-auto-promote',
      })
      .where(
        and(
          eq(websiteSubmissions.submissionId, submissionId),
          eq(websiteSubmissions.portId, portId),
        ),
      );

    return { kind: 'promoted' as const, interestId, clientId };
  });

  if (result.kind === 'skip') {
    return { promoted: false, reason: result.reason };
  }
  return {
    promoted: true,
    interestId: result.interestId,
    clientId: result.clientId,
    berthId,
    mooringNumber,
  };
}

async function createPromotedClient(
  tx: typeof db,
  portId: string,
  fullName: string,
  normalizedEmail: string,
  phone: string,
): Promise<string> {
  const [newClient] = await tx
    .insert(clients)
    .values({ portId, fullName, source: 'website' })
    .returning();
  const clientId = newClient!.id;

  await tx.insert(clientContacts).values({
    clientId,
    channel: 'email',
    value: normalizedEmail,
    isPrimary: true,
  });
  if (phone) {
    await tx.insert(clientContacts).values({
      clientId,
      channel: 'phone',
      value: phone,
      isPrimary: false,
    });
  }

  return clientId;
}
