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

import { db } from '@/lib/db';
import { berths } from '@/lib/db/schema/berths';
import { clients, clientAddresses, clientContacts } from '@/lib/db/schema/clients';
import { companies, companyAddresses } from '@/lib/db/schema/companies';
import { interests, interestBerths, interestNotes } from '@/lib/db/schema/interests';
import { ports } from '@/lib/db/schema/ports';
import { yachts } from '@/lib/db/schema/yachts';
import { getCountryName } from '@/lib/i18n/countries';
import { NotFoundError, ValidationError } from '@/lib/errors';
import { getPrimaryBerth } from '@/lib/services/interest-berths.service';
import { formatBerthRange } from '@/lib/templates/berth-range';

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

export type EoiContext = {
  client: {
    id: string;
    fullName: string;
    nationality: string | null;
    primaryEmail: string | null;
    primaryPhone: string | null;
    address: {
      street: string;
      city: string;
      /** ISO-3166-2 subdivision code stripped of its country prefix
       *  (e.g. 'NY' from 'US-NY'). Empty string when not set. */
      subdivision: string;
      postalCode: string;
      /** Localised country name - for the deprecated UI preview line only. */
      country: string;
      /** ISO-3166-1 alpha-2 country code - what the EOI Address field renders
       *  (e.g. 'US'), not the long name. */
      countryIso: string;
    } | null;
  };
  /** Optional. The EOI's Section 3 yacht block is left blank when null. */
  yacht: {
    id: string;
    name: string;
    lengthFt: string | null;
    widthFt: string | null;
    draftFt: string | null;
    lengthM: string | null;
    widthM: string | null;
    draftM: string | null;
    /** Which unit the rep originally typed in. Drives the EOI drawer's
     *  default unit toggle so a metric-entered yacht doesn't render as ft. */
    lengthUnit: 'ft' | 'm';
    widthUnit: 'ft' | 'm';
    draftUnit: 'ft' | 'm';
    hullNumber: string | null;
    flag: string | null;
    yearBuilt: number | null;
  } | null;
  company: {
    name: string;
    legalName: string | null;
    taxId: string | null;
    billingAddress: string | null;
  } | null;
  /** Inferred from the yacht's polymorphic owner. Falls back to the interest's
   *  client when no yacht is linked (so the EOI's signing party is still
   *  resolvable). */
  owner: {
    type: 'client' | 'company';
    name: string;
    legalName?: string;
  };
  /** Optional. The EOI's Section 3 berth-number is left blank when null. */
  berth: {
    mooringNumber: string;
    area: string | null;
    lengthFt: string | null;
    price: string | null;
    priceCurrency: string;
    tenureType: string;
  } | null;
  /**
   * Compact range string for every berth in the interest's EOI bundle
   * (rows where `interest_berths.is_in_eoi_bundle=true`). Populates the
   * Documenso `Berth Range` form field for multi-berth EOIs (plan §1
   * + §4.6). Empty string when the bundle is empty.
   */
  eoiBerthRange: string;
  interest: {
    stage: string;
    leadCategory: string | null;
    dateFirstContact: Date | null;
    notes: string | null;
  };
  port: {
    name: string;
    defaultCurrency: string;
  };
  date: {
    today: string;
    year: string;
  };
};

// ─── buildEoiContext ──────────────────────────────────────────────────────────

/**
 * Assembles the shared context object used by EOI generation, templates, and
 * any other flow that needs a denormalised snapshot of an interest + its
 * surrounding entities (client, yacht, berth, owner, port, etc.).
 *
 * Pure read-only: no audit logs, no socket emits, no mutations.
 *
 * Tenant-scoped: every fetch is gated by `portId`, and missing rows surface
 * as NotFoundError. The hard gate matches the EOI document's top paragraph
 * (Section 2 - name, address, email): without those the EOI is unsignable
 * and we throw. Yacht and berth (Section 3) are optional - the rendered PDF
 * leaves those fields blank when not set.
 */
export async function buildEoiContext(interestId: string, portId: string): Promise<EoiContext> {
  // 1. Interest (tenant-scoped)
  const interest = await db.query.interests.findFirst({
    where: and(eq(interests.id, interestId), eq(interests.portId, portId)),
  });
  if (!interest) {
    throw new NotFoundError('Interest');
  }

  // Resolve the interest's primary berth via the junction (plan §3.4).
  // EOI Section 3 stays blank when no primary is set.
  const primaryBerth = await getPrimaryBerth(interest.id);
  const primaryBerthId = primaryBerth?.berthId ?? null;

  // The legacy `interests.notes` blob was dropped in favour of the
  // threaded `interest_notes` timeline. Templates / merge fields still
  // expose `interest.notes`, so we surface the most-recent threaded
  // note's content here. Returns null when the interest has no notes.
  const [latestNote] = await db
    .select({ content: interestNotes.content })
    .from(interestNotes)
    .where(eq(interestNotes.interestId, interest.id))
    .orderBy(desc(interestNotes.createdAt))
    .limit(1);
  const interestNotesContent = latestNote?.content ?? null;

  // Resolve every berth in the EOI bundle (is_in_eoi_bundle=true) for the
  // multi-berth EOI compact-range merge field. Empty bundle → "" so the
  // Documenso template renders blank rather than "undefined".
  const bundleRows = await db
    .select({ mooringNumber: berths.mooringNumber })
    .from(interestBerths)
    .innerJoin(berths, eq(berths.id, interestBerths.berthId))
    .where(and(eq(interestBerths.interestId, interest.id), eq(interestBerths.isInEoiBundle, true)));
  const eoiBerthRange = formatBerthRange(bundleRows.map((r) => r.mooringNumber));

  // Parallelise independent reads. Yacht and berth are both nullable -
  // the EOI's Section 3 stays blank when they're absent.
  const [yacht, berth, client, port] = await Promise.all([
    interest.yachtId
      ? db.query.yachts.findFirst({
          where: and(eq(yachts.id, interest.yachtId), eq(yachts.portId, portId)),
        })
      : Promise.resolve(undefined),
    primaryBerthId
      ? db.query.berths.findFirst({
          where: and(eq(berths.id, primaryBerthId), eq(berths.portId, portId)),
        })
      : Promise.resolve(undefined),
    db.query.clients.findFirst({
      where: and(eq(clients.id, interest.clientId), eq(clients.portId, portId)),
    }),
    db.query.ports.findFirst({
      where: eq(ports.id, portId),
    }),
  ]);

  if (!client) throw new NotFoundError('Client');
  if (!port) throw new NotFoundError('Port');

  // 5. Primary contacts - email + phone for the interest's client.
  const contactRows = await db
    .select({
      channel: clientContacts.channel,
      value: clientContacts.value,
      isPrimary: clientContacts.isPrimary,
      updatedAt: clientContacts.updatedAt,
    })
    .from(clientContacts)
    .where(eq(clientContacts.clientId, client.id))
    .orderBy(desc(clientContacts.isPrimary), desc(clientContacts.updatedAt));

  const firstEmail = contactRows.find((c) => c.channel === 'email');
  const firstPhone =
    contactRows.find((c) => c.channel === 'phone') ??
    contactRows.find((c) => c.channel === 'whatsapp');

  // 6. Primary address. Country is rendered as the localized name (English by
  // default for documents) from the ISO code.
  const [primaryAddress] = await db
    .select({
      streetAddress: clientAddresses.streetAddress,
      city: clientAddresses.city,
      subdivisionIso: clientAddresses.subdivisionIso,
      postalCode: clientAddresses.postalCode,
      countryIso: clientAddresses.countryIso,
    })
    .from(clientAddresses)
    .where(and(eq(clientAddresses.clientId, client.id), eq(clientAddresses.isPrimary, true)))
    .limit(1);

  // The EOI's Address field renders the subdivision as the ISO-3166-2 SUFFIX
  // (e.g. 'NY' from 'US-NY') and the country as the alpha-2 code ('US') so
  // the line is short enough to fit. The localised country name is kept
  // alongside for legacy callers and the in-app preview.
  const clientAddress = primaryAddress
    ? {
        street: primaryAddress.streetAddress ?? '',
        city: primaryAddress.city ?? '',
        subdivision: primaryAddress.subdivisionIso
          ? (primaryAddress.subdivisionIso.split('-').pop() ?? '')
          : '',
        postalCode: primaryAddress.postalCode ?? '',
        country: primaryAddress.countryIso ? getCountryName(primaryAddress.countryIso, 'en') : '',
        countryIso: primaryAddress.countryIso ?? '',
      }
    : null;

  // EOI hard gate: the document's top paragraph (Section 2) requires Name,
  // Address, and Email. Without these the rendered EOI is unsignable. Yacht
  // and berth (Section 3) are intentionally optional and may be left blank.
  const missing: string[] = [];
  if (!client.fullName?.trim()) missing.push('client name');
  if (!firstEmail?.value?.trim()) missing.push('client email');
  if (!clientAddress || !clientAddress.street.trim()) missing.push('client address');
  if (missing.length > 0) {
    throw new ValidationError(
      `Cannot generate EOI - missing required client details: ${missing.join(', ')}.`,
    );
  }

  // Owner block. When a yacht is linked, derive from the yacht's polymorphic
  // owner. When no yacht is linked, fall back to the interest's client so the
  // EOI's signing party is still resolvable.
  let ownerBlock: EoiContext['owner'];
  let companyBlock: EoiContext['company'] = null;

  if (!yacht) {
    ownerBlock = { type: 'client', name: client.fullName };
  } else if (yacht.currentOwnerType === 'client') {
    // The yacht-owning client may or may not be the same as the interest's client.
    const ownerClient =
      yacht.currentOwnerId === client.id
        ? client
        : await db.query.clients.findFirst({
            where: and(eq(clients.id, yacht.currentOwnerId), eq(clients.portId, portId)),
          });
    if (!ownerClient) throw new NotFoundError('Client');
    ownerBlock = { type: 'client', name: ownerClient.fullName };
  } else if (yacht.currentOwnerType === 'company') {
    const company = await db.query.companies.findFirst({
      where: and(eq(companies.id, yacht.currentOwnerId), eq(companies.portId, portId)),
    });
    if (!company) throw new NotFoundError('Company');

    ownerBlock = {
      type: 'company',
      name: company.name,
      ...(company.legalName ? { legalName: company.legalName } : {}),
    };

    const [companyPrimaryAddress] = await db
      .select({
        streetAddress: companyAddresses.streetAddress,
        city: companyAddresses.city,
        countryIso: companyAddresses.countryIso,
      })
      .from(companyAddresses)
      .where(and(eq(companyAddresses.companyId, company.id), eq(companyAddresses.isPrimary, true)))
      .limit(1);

    const billingAddress = companyPrimaryAddress
      ? [
          companyPrimaryAddress.streetAddress,
          companyPrimaryAddress.city,
          companyPrimaryAddress.countryIso
            ? getCountryName(companyPrimaryAddress.countryIso, 'en')
            : null,
        ]
          .filter((s): s is string => Boolean(s))
          .join(', ') || null
      : null;

    companyBlock = {
      name: company.name,
      legalName: company.legalName,
      taxId: company.taxId,
      billingAddress,
    };
  } else {
    throw new ValidationError(`unknown yacht owner type: ${String(yacht.currentOwnerType)}`);
  }

  // 10. Date.
  const now = new Date();
  const today = now.toISOString().slice(0, 10);
  const year = String(now.getFullYear());

  // CM-2 Part B: deal-specific price override. The base berth price is the
  // canonical list price; an interest_berths.price_override (when set)
  // supersedes it for THIS interest's documents via the existing
  // {{berth.price}} / {{berth.priceCurrency}} tokens.
  let resolvedBerthPrice = berth?.price ?? null;
  let resolvedBerthCurrency = berth?.priceCurrency ?? port.defaultCurrency;
  if (berth && primaryBerthId) {
    const [ibOverride] = await db
      .select({
        priceOverride: interestBerths.priceOverride,
        priceOverrideCurrency: interestBerths.priceOverrideCurrency,
      })
      .from(interestBerths)
      .where(
        and(eq(interestBerths.interestId, interest.id), eq(interestBerths.berthId, primaryBerthId)),
      );
    if (ibOverride?.priceOverride != null) {
      resolvedBerthPrice = ibOverride.priceOverride;
      resolvedBerthCurrency = ibOverride.priceOverrideCurrency ?? berth.priceCurrency;
    }
  }

  return {
    client: {
      id: client.id,
      fullName: client.fullName,
      nationality: client.nationalityIso ? getCountryName(client.nationalityIso, 'en') : null,
      primaryEmail: firstEmail?.value ?? null,
      primaryPhone: firstPhone?.value ?? null,
      address: clientAddress,
    },
    yacht: yacht
      ? {
          id: yacht.id,
          name: yacht.name,
          lengthFt: yacht.lengthFt,
          widthFt: yacht.widthFt,
          draftFt: yacht.draftFt,
          lengthM: yacht.lengthM,
          widthM: yacht.widthM,
          draftM: yacht.draftM,
          lengthUnit: (yacht.lengthUnit ?? 'ft') as 'ft' | 'm',
          widthUnit: (yacht.widthUnit ?? 'ft') as 'ft' | 'm',
          draftUnit: (yacht.draftUnit ?? 'ft') as 'ft' | 'm',
          hullNumber: yacht.hullNumber,
          flag: yacht.flag,
          yearBuilt: yacht.yearBuilt,
        }
      : null,
    company: companyBlock,
    owner: ownerBlock,
    berth: berth
      ? {
          mooringNumber: berth.mooringNumber,
          area: berth.area,
          lengthFt: berth.lengthFt,
          price: resolvedBerthPrice,
          priceCurrency: resolvedBerthCurrency,
          tenureType: berth.tenureType,
        }
      : null,
    eoiBerthRange,
    interest: {
      stage: interest.pipelineStage,
      leadCategory: interest.leadCategory,
      dateFirstContact: interest.dateFirstContact,
      notes: interestNotesContent,
    },
    port: {
      name: port.name,
      defaultCurrency: port.defaultCurrency,
    },
    date: {
      today,
      year,
    },
  };
}
