/**
 * Smart-archive dossier service.
 *
 * Returns a structured snapshot of "what's at stake" when archiving (or
 * restoring) a client, so the UI can render the wizard with the right
 * sections + populated decision points.
 *
 * The dossier is read-only and side-effect-free. Any mutation happens
 * via the companion `client-archive.service.ts` which takes the user's
 * decisions and applies them inside a single transaction.
 */

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

import { db } from '@/lib/db';
import { clients } from '@/lib/db/schema/clients';
import { yachts } from '@/lib/db/schema/yachts';
import { companies, companyMemberships } from '@/lib/db/schema/companies';
import { interests, interestBerths } from '@/lib/db/schema/interests';
import { berths } from '@/lib/db/schema/berths';
import { berthTenancies } from '@/lib/db/schema/tenancies';
import { invoices } from '@/lib/db/schema/financial';
import { documents } from '@/lib/db/schema/documents';
import { activeInterestsWhere } from '@/lib/services/active-interest';
import { portalUsers } from '@/lib/db/schema/portal';
import { NotFoundError } from '@/lib/errors';
import type { PipelineStage } from '@/lib/constants';

// ─── Public types ───────────────────────────────────────────────────────────

/**
 * Pipeline stages that count as "high-stakes" for the bulk wizard:
 *   Past these, money has changed hands or a contract is in motion. The
 *   bulk-archive UI prompts the operator to confirm individually + supply
 *   a reason for these clients.
 */
export const HIGH_STAKES_STAGES: ReadonlySet<PipelineStage> = new Set<PipelineStage>([
  'deposit_paid',
  'contract',
]);

export type ArchiveStakeLevel = 'low' | 'high';

/** A berth currently linked to one of the client's interests. */
export interface DossierBerth {
  berthId: string;
  mooringNumber: string;
  status: string; // 'available' | 'under_offer' | 'sold'
  /** Every interest of THIS client that links to the berth. The bulk
   *  wizard uses this to pick the right interestId per berth instead of
   *  guessing by primary-mooring (which fails when multiple interests
   *  share a primary or when none is primary). */
  linkedInterestIds: string[];
  /** Other interests still actively expressing interest in this berth
   *  (so the next-in-line notification can list them). */
  otherInterests: Array<{
    interestId: string;
    clientId: string | null;
    clientName: string | null;
    pipelineStage: string;
    daysSinceUpdate: number;
  }>;
}

export interface DossierDocument {
  documentId: string;
  templateName: string | null;
  status: string; // 'draft' | 'sent' | 'signed' | 'voided' | ...
  documensoEnvelopeId: string | null;
  /** True when there's a live envelope in Documenso awaiting signature. */
  isInFlight: boolean;
}

export interface DossierYacht {
  yachtId: string;
  name: string;
  hullNumber: string | null;
  status: string;
}

export interface DossierCompany {
  companyId: string;
  name: string;
  membershipRole: string | null;
}

export interface DossierTenancy {
  tenancyId: string;
  berthId: string;
  mooringNumber: string;
  status: string; // typically 'active'
  startDate: string;
}

export interface DossierInvoice {
  invoiceId: string;
  invoiceNumber: string;
  status: string;
  total: string;
  currency: string;
}

export interface DossierInterest {
  interestId: string;
  pipelineStage: PipelineStage;
  primaryBerthMooring: string | null;
  hasSignedEoi: boolean;
}

export interface ClientArchiveDossier {
  client: {
    id: string;
    fullName: string;
    portId: string;
    archivedAt: string | null;
  };
  /** The headline classification - drives whether the bulk wizard
   *  treats this client as low-stakes (auto) or high-stakes (per-row
   *  confirmation + reason required). */
  stakeLevel: ArchiveStakeLevel;
  /** The interest stage that earned the high-stakes classification (so
   *  the UI can explain "this client is in Deposit Paid, please confirm").
   *  Null when low-stakes. */
  highStakesStage: PipelineStage | null;

  // Sections - empty arrays mean "nothing to handle in this category"
  interests: DossierInterest[];
  berths: DossierBerth[];
  yachts: DossierYacht[];
  companies: DossierCompany[];
  tenancies: DossierTenancy[];
  invoices: DossierInvoice[];
  documents: DossierDocument[];
  hasPortalUser: boolean;

  /** Hard blockers - cannot proceed with archive at all until these are
   *  resolved manually. Currently the only one is "active tenancy
   *  on a sold berth" (since you can't unsell a berth from this flow). */
  blockers: string[];
}

// ─── Implementation ──────────────────────────────────────────────────────────

const DAY_MS = 24 * 60 * 60 * 1000;

/**
 * Loads the full archive dossier for one client. Caller (route handler)
 * is responsible for the tenant gate.
 */
export async function getClientArchiveDossier(
  clientId: string,
  portId: string,
): Promise<ClientArchiveDossier> {
  const [client] = await db
    .select({
      id: clients.id,
      fullName: clients.fullName,
      portId: clients.portId,
      archivedAt: clients.archivedAt,
    })
    .from(clients)
    .where(and(eq(clients.id, clientId), eq(clients.portId, portId)))
    .limit(1);

  if (!client) throw new NotFoundError('client');

  // ─── Interests + stake classification ────────────────────────────────────
  const clientInterests = await db
    .select({
      id: interests.id,
      pipelineStage: interests.pipelineStage,
    })
    .from(interests)
    .where(and(eq(interests.clientId, clientId), isNull(interests.archivedAt)));

  let stakeLevel: ArchiveStakeLevel = 'low';
  let highStakesStage: PipelineStage | null = null;
  for (const i of clientInterests) {
    if (HIGH_STAKES_STAGES.has(i.pipelineStage as PipelineStage)) {
      stakeLevel = 'high';
      // Pick the highest-stage one to surface in the UI message.
      if (
        !highStakesStage ||
        rankStage(i.pipelineStage as PipelineStage) > rankStage(highStakesStage)
      ) {
        highStakesStage = i.pipelineStage as PipelineStage;
      }
    }
  }

  // ─── Documents (signed EOIs trigger the acknowledgment requirement) ─────
  const clientDocuments = await db
    .select({
      id: documents.id,
      title: documents.title,
      documentType: documents.documentType,
      status: documents.status,
      documensoId: documents.documensoId,
    })
    .from(documents)
    .where(and(eq(documents.clientId, clientId), eq(documents.portId, portId)));

  const dossierDocs: DossierDocument[] = clientDocuments.map((d) => ({
    documentId: d.id,
    templateName: d.title,
    status: d.status,
    documensoEnvelopeId: d.documensoId,
    isInFlight: !!d.documensoId && (d.status === 'sent' || d.status === 'partially_signed'),
  }));

  const interestsWithSignedEoi = new Set<string>(
    clientDocuments.filter((d) => d.status === 'completed').map((d) => d.id),
  );

  // ─── Interests + berth links ─────────────────────────────────────────────
  const interestIds = clientInterests.map((i) => i.id);
  const interestBerthRows = interestIds.length
    ? await db
        .select({
          interestId: interestBerths.interestId,
          berthId: interestBerths.berthId,
          isPrimary: interestBerths.isPrimary,
          mooringNumber: berths.mooringNumber,
          berthStatus: berths.status,
        })
        .from(interestBerths)
        .innerJoin(berths, eq(interestBerths.berthId, berths.id))
        .where(inArray(interestBerths.interestId, interestIds))
    : [];

  const dossierInterests: DossierInterest[] = clientInterests.map((i) => {
    const primary = interestBerthRows.find((r) => r.interestId === i.id && r.isPrimary);
    return {
      interestId: i.id,
      pipelineStage: i.pipelineStage as PipelineStage,
      primaryBerthMooring: primary?.mooringNumber ?? null,
      hasSignedEoi: interestsWithSignedEoi.has(i.id),
    };
  });

  // ─── Berths section + next-in-line interests ────────────────────────────
  const distinctBerthIds = Array.from(new Set(interestBerthRows.map((r) => r.berthId)));
  const dossierBerths: DossierBerth[] = [];
  for (const berthId of distinctBerthIds) {
    const berth = interestBerthRows.find((r) => r.berthId === berthId);
    if (!berth) continue;

    // "Other interests" = interest_berths rows on this berth that DON'T
    // belong to the client being archived AND whose interest is still
    // active (no outcome set, not archived). Surfaces who the sales
    // rep should reach out to next.
    const others = await db
      .select({
        interestId: interests.id,
        clientId: interests.clientId,
        clientName: clients.fullName,
        pipelineStage: interests.pipelineStage,
        updatedAt: interests.updatedAt,
      })
      .from(interestBerths)
      .innerJoin(interests, eq(interestBerths.interestId, interests.id))
      .leftJoin(clients, eq(interests.clientId, clients.id))
      .where(
        and(
          activeInterestsWhere(portId),
          eq(interestBerths.berthId, berthId),
          ne(interests.clientId, clientId),
        ),
      )
      .orderBy(desc(interests.updatedAt))
      .limit(10);

    // Every linked interest belonging to THIS client (multiple
    // interests can share a berth - primary flag is at most one per
    // interest, not per berth).
    const linkedInterestIds = Array.from(
      new Set(interestBerthRows.filter((r) => r.berthId === berthId).map((r) => r.interestId)),
    );

    dossierBerths.push({
      berthId,
      mooringNumber: berth.mooringNumber,
      status: berth.berthStatus,
      linkedInterestIds,
      otherInterests: others.map((o) => ({
        interestId: o.interestId,
        clientId: o.clientId,
        clientName: o.clientName,
        pipelineStage: o.pipelineStage,
        daysSinceUpdate: Math.floor((Date.now() - new Date(o.updatedAt).getTime()) / DAY_MS),
      })),
    });
  }

  // ─── Yachts owned by client ──────────────────────────────────────────────
  const ownedYachts = await db
    .select({
      id: yachts.id,
      name: yachts.name,
      hullNumber: yachts.hullNumber,
      status: yachts.status,
    })
    .from(yachts)
    .where(
      and(
        eq(yachts.portId, portId),
        eq(yachts.currentOwnerType, 'client'),
        eq(yachts.currentOwnerId, clientId),
        isNull(yachts.archivedAt),
      ),
    );

  // ─── Company memberships (current - no end_date) ─────────────────────────
  const memberRows = await db
    .select({
      companyId: companies.id,
      name: companies.name,
      role: companyMemberships.role,
    })
    .from(companyMemberships)
    .innerJoin(companies, eq(companyMemberships.companyId, companies.id))
    .where(
      and(
        eq(companyMemberships.clientId, clientId),
        eq(companies.portId, portId),
        isNull(companyMemberships.endDate),
      ),
    );

  // ─── Active tenancies ────────────────────────────────────────────────────
  const activeTenancies = await db
    .select({
      id: berthTenancies.id,
      berthId: berthTenancies.berthId,
      mooringNumber: berths.mooringNumber,
      status: berthTenancies.status,
      startDate: berthTenancies.startDate,
      berthStatus: berths.status,
    })
    .from(berthTenancies)
    .innerJoin(berths, eq(berthTenancies.berthId, berths.id))
    .where(
      and(
        eq(berthTenancies.clientId, clientId),
        eq(berthTenancies.portId, portId),
        eq(berthTenancies.status, 'active'),
      ),
    );

  // ─── Outstanding invoices (anything not paid / not cancelled) ────────────
  const outstandingInvoices = await db
    .select({
      id: invoices.id,
      invoiceNumber: invoices.invoiceNumber,
      status: invoices.status,
      total: invoices.total,
      currency: invoices.currency,
    })
    .from(invoices)
    .where(
      and(
        eq(invoices.portId, portId),
        eq(invoices.billingEntityType, 'client'),
        eq(invoices.billingEntityId, clientId),
        isNull(invoices.archivedAt),
        ne(invoices.status, 'paid'),
        ne(invoices.status, 'cancelled'),
      ),
    );

  // ─── Portal user existence ───────────────────────────────────────────────
  const [portalUser] = await db
    .select({ id: portalUsers.id })
    .from(portalUsers)
    .where(and(eq(portalUsers.clientId, clientId), eq(portalUsers.portId, portId)))
    .limit(1);

  // ─── Hard blockers ───────────────────────────────────────────────────────
  // The only true blocker is an active tenancy on a SOLD berth - we
  // can't auto-handle this without crossing into refund territory. Force
  // the operator to handle it via the existing tenancy UI first.
  const blockers: string[] = [];
  for (const r of activeTenancies) {
    if (r.berthStatus === 'sold') {
      blockers.push(
        `Active tenancy on sold berth ${r.mooringNumber} (#${r.id.slice(0, 8)}). Process the refund or transfer the tenancy before archiving.`,
      );
    }
  }

  return {
    client: {
      id: client.id,
      fullName: client.fullName,
      portId: client.portId,
      archivedAt: client.archivedAt ? client.archivedAt.toISOString() : null,
    },
    stakeLevel,
    highStakesStage,
    interests: dossierInterests,
    berths: dossierBerths,
    yachts: ownedYachts.map((y) => ({
      yachtId: y.id,
      name: y.name,
      hullNumber: y.hullNumber,
      status: y.status,
    })),
    companies: memberRows.map((m) => ({
      companyId: m.companyId,
      name: m.name,
      membershipRole: m.role,
    })),
    tenancies: activeTenancies.map((r) => ({
      tenancyId: r.id,
      berthId: r.berthId,
      mooringNumber: r.mooringNumber,
      status: r.status,
      startDate: r.startDate.toISOString(),
    })),
    invoices: outstandingInvoices.map((i) => ({
      invoiceId: i.id,
      invoiceNumber: i.invoiceNumber,
      status: i.status,
      total: i.total,
      currency: i.currency,
    })),
    documents: dossierDocs,
    hasPortalUser: !!portalUser,
    blockers,
  };
}

// Stage rank used to pick the "highest" high-stakes stage when surfacing
// the warning copy. Higher = more committed. Doc sub-status is folded back
// in via the caller (treating eoi+signed as past nurturing, contract+signed
// as the apex).
function rankStage(s: PipelineStage): number {
  switch (s) {
    case 'contract':
      return 4;
    case 'deposit_paid':
      return 3;
    case 'reservation':
      return 2;
    case 'eoi':
      return 1;
    default:
      return 0;
  }
}
