/**
 * Phase 3b - EOI field-override side-effects + persistence.
 *
 * The EOI dialog lets reps override pre-filled fields (email, phone,
 * yacht name) with one of three intents:
 *
 *   1. **Use only for this EOI** (`useOnlyForThisEoi=true`)
 *      → write to `documents.override_*` columns only; never mutate
 *        client_contacts or yachts. Future EOIs revert to the canonical
 *        primary.
 *
 *   2. **Set as default for future docs** (`setAsDefault=true`)
 *      → promote an existing `client_contacts` row to primary, or insert
 *        + promote if the rep typed a fresh value. Demote the prior
 *        primary inside the same transaction. `documents.override_*`
 *        stays NULL because the canonical record now matches.
 *
 *   3. **Neither flag** (default - rep picked a secondary from the
 *      combobox OR typed something fresh)
 *      → if the value is fresh (no `contactId`), insert a non-primary
 *        `client_contacts` row (`source='eoi-custom-input'`,
 *        `source_document_id=<this EOI>`). Either way write
 *        `documents.override_*` so the rendered doc records the
 *        deviation from the canonical primary.
 *
 * Yacht name overrides have no contact-row analog. `useOnlyForThisEoi`
 * writes to `documents.override_yacht_name`; `setAsDefault` patches the
 * canonical `yachts.name` column.
 *
 * The applied override values are returned so the caller can layer them
 * onto the in-memory EOI context before rendering - without a separate
 * round-trip to re-read the freshly-mutated contact rows.
 */

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

import { db } from '@/lib/db';
import { clients, clientAddresses, clientContacts } from '@/lib/db/schema/clients';
import { documents } from '@/lib/db/schema/documents';
import { interests } from '@/lib/db/schema/interests';
import { yachts } from '@/lib/db/schema/yachts';
import { createAuditLog, type AuditMeta } from '@/lib/audit';
import { ValidationError } from '@/lib/errors';
import { withTransaction } from '@/lib/db/utils';

export interface FieldOverrideInput {
  value: string;
  useOnlyForThisEoi: boolean;
  setAsDefault: boolean;
  contactId?: string | null;
}

export interface AddressOverrideInput {
  line1?: string;
  line2?: string;
  city?: string;
  subdivisionIso?: string;
  postalCode?: string;
  countryIso?: string;
  useOnlyForThisEoi: boolean;
  setAsDefault: boolean;
  /** Existing client_addresses.id when the rep picked one from a list;
   *  null = fresh values typed in the dialog. */
  addressId?: string | null;
}

export interface EoiOverridesInput {
  clientEmail?: FieldOverrideInput;
  clientPhone?: FieldOverrideInput;
  yachtName?: FieldOverrideInput;
  clientAddress?: AddressOverrideInput;
}

export interface AppliedOverrides {
  /** Values to layer onto the in-memory EoiContext before rendering. */
  resolved: {
    clientEmail?: string;
    clientPhone?: string;
    yachtName?: string;
    clientAddress?: {
      line1: string;
      line2: string;
      city: string;
      subdivisionIso: string;
      postalCode: string;
      countryIso: string;
    };
  };
  /** Columns to write to `documents.override_*` after the doc row exists.
   *  Empty when every override either ran `setAsDefault` (canonical
   *  updated) or no overrides were supplied. */
  documentOverrideColumns: Partial<{
    overrideClientEmail: string;
    overrideClientPhone: string;
    overrideYachtName: string;
    overrideClientAddressLine1: string;
    overrideClientAddressLine2: string;
    overrideClientCity: string;
    overrideClientState: string;
    overrideClientPostalCode: string;
    overrideClientCountry: string;
  }>;
}

/**
 * Apply override side-effects (insert contacts, promote primaries,
 * patch yacht name) and return the values to be used at render time.
 *
 * Runs all mutations in a single transaction so a partial failure
 * (e.g. setAsDefault promotion succeeds for email but fails for
 * phone) doesn't leave the contact table in a split-brain state.
 *
 * Audit log entries: `eoi_field_override` per field touched.
 */
export async function applyEoiOverridesBeforeRender(
  portId: string,
  interestId: string,
  overrides: EoiOverridesInput | undefined,
  meta: AuditMeta,
): Promise<AppliedOverrides> {
  const empty: AppliedOverrides = { resolved: {}, documentOverrideColumns: {} };
  if (!overrides) return empty;

  // Resolve the interest's client (for contact mutations) and yacht (for
  // yacht-name mutations) up-front so the transaction body has everything
  // it needs without re-fetching.
  const interest = await db.query.interests.findFirst({
    where: and(eq(interests.id, interestId), eq(interests.portId, portId)),
  });
  if (!interest) throw new ValidationError('interest not found for overrides');

  const client = await db.query.clients.findFirst({
    where: and(eq(clients.id, interest.clientId), eq(clients.portId, portId)),
  });
  if (!client) throw new ValidationError('client not found for overrides');

  const yacht = interest.yachtId
    ? await db.query.yachts.findFirst({
        where: and(eq(yachts.id, interest.yachtId), eq(yachts.portId, portId)),
      })
    : null;

  // ─── Single transaction wrapping every side-effect ────────────────────────
  return withTransaction(async (tx) => {
    const resolved: AppliedOverrides['resolved'] = {};
    const documentOverrideColumns: AppliedOverrides['documentOverrideColumns'] = {};

    // Helper for contact-channel overrides (email + phone share logic).
    const applyContactOverride = async (
      override: FieldOverrideInput,
      channel: 'email' | 'phone',
      docColumn: 'overrideClientEmail' | 'overrideClientPhone',
    ): Promise<string> => {
      const value = override.value.trim();
      if (!value) throw new ValidationError(`${channel} override value cannot be empty`);

      if (override.useOnlyForThisEoi) {
        // No contact mutation. Override applies only to this document.
        documentOverrideColumns[docColumn] = value;
        return value;
      }

      if (override.setAsDefault) {
        // Promote: either an existing contactId or a fresh insert. Demote
        // the prior primary for the same channel first so the partial
        // unique index doesn't reject the promotion.
        await tx
          .update(clientContacts)
          .set({ isPrimary: false, updatedAt: new Date() })
          .where(
            and(
              eq(clientContacts.clientId, client.id),
              eq(clientContacts.channel, channel),
              eq(clientContacts.isPrimary, true),
            ),
          );

        if (override.contactId) {
          // Promote existing row.
          await tx
            .update(clientContacts)
            .set({ isPrimary: true, value, updatedAt: new Date() })
            .where(
              and(
                eq(clientContacts.id, override.contactId),
                eq(clientContacts.clientId, client.id),
              ),
            );
        } else {
          // Fresh insert + primary.
          await tx.insert(clientContacts).values({
            clientId: client.id,
            channel,
            value,
            isPrimary: true,
            source: 'eoi-custom-input',
          });
        }

        // Canonical now matches → documents.override_* stays NULL.
        return value;
      }

      // Neither flag set. If the rep picked an existing contact row
      // (contactId set) we don't mutate; if they typed a fresh value
      // we insert a non-primary contact so it shows up in future
      // dropdowns. Either way we record the deviation on the document.
      if (!override.contactId) {
        await tx.insert(clientContacts).values({
          clientId: client.id,
          channel,
          value,
          isPrimary: false,
          source: 'eoi-custom-input',
        });
      }
      documentOverrideColumns[docColumn] = value;
      return value;
    };

    if (overrides.clientEmail) {
      resolved.clientEmail = await applyContactOverride(
        overrides.clientEmail,
        'email',
        'overrideClientEmail',
      );
    }

    if (overrides.clientPhone) {
      resolved.clientPhone = await applyContactOverride(
        overrides.clientPhone,
        'phone',
        'overrideClientPhone',
      );
    }

    if (overrides.yachtName) {
      const value = overrides.yachtName.value.trim();
      if (!value) throw new ValidationError('yacht name override cannot be empty');
      if (!yacht) {
        // Yacht-name override without a linked yacht only makes sense
        // for the per-document path - otherwise there's no canonical
        // record to update.
        if (overrides.yachtName.setAsDefault) {
          throw new ValidationError('cannot setAsDefault for yacht name when no yacht is linked');
        }
        documentOverrideColumns.overrideYachtName = value;
      } else if (overrides.yachtName.useOnlyForThisEoi) {
        documentOverrideColumns.overrideYachtName = value;
      } else if (overrides.yachtName.setAsDefault) {
        await tx
          .update(yachts)
          .set({ name: value, updatedAt: new Date() })
          .where(eq(yachts.id, yacht.id));
      } else {
        // Default behaviour: per-document override.
        documentOverrideColumns.overrideYachtName = value;
      }
      resolved.yachtName = value;
    }

    if (overrides.clientAddress) {
      const a = overrides.clientAddress;
      const resolvedAddr = {
        line1: (a.line1 ?? '').trim(),
        line2: (a.line2 ?? '').trim(),
        city: (a.city ?? '').trim(),
        subdivisionIso: (a.subdivisionIso ?? '').trim(),
        postalCode: (a.postalCode ?? '').trim(),
        countryIso: (a.countryIso ?? '').trim().toUpperCase(),
      };
      // Treat the address as one logical field - at least line1 + countryIso
      // must be present for an EOI to render legally.
      if (!resolvedAddr.line1 || !resolvedAddr.countryIso) {
        throw new ValidationError('address override requires line1 and countryIso');
      }

      if (a.useOnlyForThisEoi) {
        documentOverrideColumns.overrideClientAddressLine1 = resolvedAddr.line1;
        if (resolvedAddr.line2)
          documentOverrideColumns.overrideClientAddressLine2 = resolvedAddr.line2;
        if (resolvedAddr.city) documentOverrideColumns.overrideClientCity = resolvedAddr.city;
        if (resolvedAddr.subdivisionIso)
          documentOverrideColumns.overrideClientState = resolvedAddr.subdivisionIso;
        if (resolvedAddr.postalCode)
          documentOverrideColumns.overrideClientPostalCode = resolvedAddr.postalCode;
        documentOverrideColumns.overrideClientCountry = resolvedAddr.countryIso;
      } else if (a.setAsDefault) {
        // Promote: demote the prior primary, then either update an existing
        // address row (when addressId was provided) or insert a fresh one.
        await tx
          .update(clientAddresses)
          .set({ isPrimary: false, updatedAt: new Date() })
          .where(and(eq(clientAddresses.clientId, client.id), eq(clientAddresses.isPrimary, true)));
        if (a.addressId) {
          await tx
            .update(clientAddresses)
            .set({
              // client_addresses has no addressLine2 column - concat line1+line2.
              streetAddress: resolvedAddr.line2
                ? `${resolvedAddr.line1}\n${resolvedAddr.line2}`
                : resolvedAddr.line1,
              city: resolvedAddr.city || null,
              subdivisionIso: resolvedAddr.subdivisionIso || null,
              postalCode: resolvedAddr.postalCode || null,
              countryIso: resolvedAddr.countryIso,
              isPrimary: true,
              updatedAt: new Date(),
            })
            .where(
              and(eq(clientAddresses.id, a.addressId), eq(clientAddresses.clientId, client.id)),
            );
        } else {
          await tx.insert(clientAddresses).values({
            clientId: client.id,
            portId: client.portId,
            // client_addresses has no addressLine2 column - concat line1+line2.
            streetAddress: resolvedAddr.line2
              ? `${resolvedAddr.line1}\n${resolvedAddr.line2}`
              : resolvedAddr.line1,
            city: resolvedAddr.city || null,
            subdivisionIso: resolvedAddr.subdivisionIso || null,
            postalCode: resolvedAddr.postalCode || null,
            countryIso: resolvedAddr.countryIso,
            isPrimary: true,
            source: 'eoi-custom-input',
          });
        }
        // Canonical now matches → documents.override_* stays NULL.
      } else {
        // Neither flag: persist per-doc + (if no addressId) insert a
        // non-primary address row for future reuse.
        if (!a.addressId) {
          await tx.insert(clientAddresses).values({
            clientId: client.id,
            portId: client.portId,
            streetAddress: resolvedAddr.line2
              ? `${resolvedAddr.line1}\n${resolvedAddr.line2}`
              : resolvedAddr.line1,
            city: resolvedAddr.city || null,
            subdivisionIso: resolvedAddr.subdivisionIso || null,
            postalCode: resolvedAddr.postalCode || null,
            countryIso: resolvedAddr.countryIso,
            isPrimary: false,
            source: 'eoi-custom-input',
          });
        }
        documentOverrideColumns.overrideClientAddressLine1 = resolvedAddr.line1;
        if (resolvedAddr.line2)
          documentOverrideColumns.overrideClientAddressLine2 = resolvedAddr.line2;
        if (resolvedAddr.city) documentOverrideColumns.overrideClientCity = resolvedAddr.city;
        if (resolvedAddr.subdivisionIso)
          documentOverrideColumns.overrideClientState = resolvedAddr.subdivisionIso;
        if (resolvedAddr.postalCode)
          documentOverrideColumns.overrideClientPostalCode = resolvedAddr.postalCode;
        documentOverrideColumns.overrideClientCountry = resolvedAddr.countryIso;
      }
      resolved.clientAddress = resolvedAddr;
    }

    // One audit row per touched field summarising the override intent.
    const auditFields: Array<{ field: string; intent: Record<string, unknown> }> = [];
    if (overrides.clientEmail)
      auditFields.push({
        field: 'clientEmail',
        intent: {
          value: overrides.clientEmail.value.slice(0, 200),
          useOnlyForThisEoi: overrides.clientEmail.useOnlyForThisEoi,
          setAsDefault: overrides.clientEmail.setAsDefault,
          fromContactId: overrides.clientEmail.contactId ?? null,
        },
      });
    if (overrides.clientPhone)
      auditFields.push({
        field: 'clientPhone',
        intent: {
          value: overrides.clientPhone.value.slice(0, 200),
          useOnlyForThisEoi: overrides.clientPhone.useOnlyForThisEoi,
          setAsDefault: overrides.clientPhone.setAsDefault,
          fromContactId: overrides.clientPhone.contactId ?? null,
        },
      });
    if (overrides.yachtName)
      auditFields.push({
        field: 'yachtName',
        intent: {
          value: overrides.yachtName.value.slice(0, 200),
          useOnlyForThisEoi: overrides.yachtName.useOnlyForThisEoi,
          setAsDefault: overrides.yachtName.setAsDefault,
        },
      });
    if (overrides.clientAddress)
      auditFields.push({
        field: 'clientAddress',
        intent: {
          useOnlyForThisEoi: overrides.clientAddress.useOnlyForThisEoi,
          setAsDefault: overrides.clientAddress.setAsDefault,
          fromAddressId: overrides.clientAddress.addressId ?? null,
          countryIso: overrides.clientAddress.countryIso,
        },
      });

    for (const { field, intent } of auditFields) {
      void createAuditLog({
        userId: meta.userId,
        portId,
        action: 'eoi_field_override',
        entityType: 'interest',
        entityId: interestId,
        newValue: { field, ...intent },
        ipAddress: meta.ipAddress,
        userAgent: meta.userAgent,
      });
    }

    return { resolved, documentOverrideColumns };
  });
}

/**
 * Persist `documents.override_*` columns after the document row has
 * been inserted. No-op when no columns are set.
 *
 * `source_document_id` on any client_contacts rows inserted by the
 * preceding `applyEoiOverridesBeforeRender` call is left NULL until
 * this point - the document id doesn't exist yet during the contact
 * insert. This function backfills it.
 */
export async function persistDocumentOverrides(
  documentId: string,
  applied: AppliedOverrides,
  meta: AuditMeta,
): Promise<void> {
  const cols = applied.documentOverrideColumns;
  // Even when cols is empty (every override ran setAsDefault), we still
  // need to backfill source_document_id on freshly-inserted contact /
  // address / yacht rows whose insertion preceded the document row's
  // existence. Skip only when applied is the empty default.
  const hasResolved = Object.keys(applied.resolved).length > 0;
  if (Object.keys(cols).length > 0) {
    await db.update(documents).set(cols).where(eq(documents.id, documentId));
  } else if (!hasResolved) {
    return;
  }

  // Backfill source_document_id on freshly-inserted contact + address +
  // yacht rows from this generation pass. Bounded by createdAt < 1 min
  // so re-runs don't sweep older orphans. Done outside the override
  // transaction because the document id wasn't known yet at that point.
  await db
    .update(clientContacts)
    .set({ sourceDocumentId: documentId })
    .where(
      and(
        eq(clientContacts.source, 'eoi-custom-input'),
        sql`${clientContacts.createdAt} > NOW() - INTERVAL '1 minute'`,
        sql`${clientContacts.sourceDocumentId} IS NULL`,
      ),
    );
  await db
    .update(clientAddresses)
    .set({ sourceDocumentId: documentId })
    .where(
      and(
        eq(clientAddresses.source, 'eoi-custom-input'),
        sql`${clientAddresses.createdAt} > NOW() - INTERVAL '1 minute'`,
        sql`${clientAddresses.sourceDocumentId} IS NULL`,
      ),
    );
  // Phase 3 follow-up - yacht spawn from EOI runs BEFORE generateAndSign
  // so the yacht row's source_document_id is NULL at insert time. Same
  // bounded backfill pattern as contacts.
  await db
    .update(yachts)
    .set({ sourceDocumentId: documentId })
    .where(
      and(
        eq(yachts.source, 'eoi-generated'),
        sql`${yachts.createdAt} > NOW() - INTERVAL '1 minute'`,
        sql`${yachts.sourceDocumentId} IS NULL`,
      ),
    );
  void createAuditLog({
    userId: meta.userId,
    portId: meta.portId,
    action: 'update',
    entityType: 'document',
    entityId: documentId,
    metadata: { action: 'persist_eoi_overrides', columns: Object.keys(cols) },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });
}

/**
 * Layer applied override values onto an EOI context object so the
 * renderer (in-app pdf-lib OR Documenso payload) sees the override
 * values instead of the canonical record. Mutates the supplied object
 * (cheap; the caller built it).
 */
export function applyOverridesToContext<
  T extends {
    client: {
      primaryEmail: string | null;
      primaryPhone: string | null;
      address: {
        street: string;
        city: string;
        subdivision: string;
        postalCode: string;
        country: string;
        countryIso: string;
      } | null;
    };
    yacht: { name: string } | null;
  },
>(context: T, applied: AppliedOverrides): T {
  if (applied.resolved.clientEmail !== undefined) {
    context.client.primaryEmail = applied.resolved.clientEmail;
  }
  if (applied.resolved.clientPhone !== undefined) {
    context.client.primaryPhone = applied.resolved.clientPhone;
  }
  if (applied.resolved.yachtName !== undefined && context.yacht) {
    context.yacht.name = applied.resolved.yachtName;
  }
  if (applied.resolved.clientAddress) {
    const a = applied.resolved.clientAddress;
    const combinedStreet = a.line2 ? `${a.line1}\n${a.line2}` : a.line1;
    // Strip the country-code prefix from the subdivision ISO so the EOI
    // renders the subdivision suffix exactly the way the canonical
    // address pipeline does (e.g. 'US-CA' → 'CA').
    const subdivisionSuffix = a.subdivisionIso.includes('-')
      ? a.subdivisionIso.split('-').slice(1).join('-')
      : a.subdivisionIso;
    context.client.address = {
      street: combinedStreet,
      city: a.city,
      subdivision: subdivisionSuffix,
      postalCode: a.postalCode,
      // `country` (long name) is only used by the deprecated UI preview
      // line; the EOI's Address field renders countryIso, so we set them
      // consistently and leave the long-name lookup to the renderer.
      country: a.countryIso,
      countryIso: a.countryIso,
    };
  }
  return context;
}
