/**
 * Smart-archive mutation service.
 *
 * Takes a fully-resolved set of decisions from the UI (built off the
 * dossier) and applies them inside a single transaction. Records every
 * decision into clients.archive_metadata so the restore wizard can
 * later attempt reversal.
 *
 * External-system cleanup (Documenso envelope void/delete, mass email
 * notifications to next-in-line interests) happens AFTER the local
 * commit - best-effort, queued for retry, never blocks the archive.
 */

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

import { db } from '@/lib/db';
import { clients } from '@/lib/db/schema/clients';
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 { yachts } from '@/lib/db/schema/yachts';
import { companyMemberships } from '@/lib/db/schema/companies';
import { portalUsers } from '@/lib/db/schema/portal';
import { documents } from '@/lib/db/schema/documents';
import { createAuditLog, type AuditMeta } from '@/lib/audit';
import { ConflictError, NotFoundError, ValidationError } from '@/lib/errors';
import {
  HIGH_STAKES_STAGES,
  type ClientArchiveDossier,
} from '@/lib/services/client-archive-dossier.service';
import { activeInterestsWhere } from '@/lib/services/active-interest';
import { transferOwnershipTx } from '@/lib/services/yachts.service';

// ─── Decision payload (what the UI sends to the server) ─────────────────────

/** Per-berth choice. `interestId` is the interest in the archived client
 *  that owns the berth link (used to remove the right interestBerths row). */
export type BerthDecision = {
  berthId: string;
  interestId: string;
  action: 'release' | 'retain';
};

export type YachtDecision = {
  yachtId: string;
  action: 'transfer' | 'mark_sold_away' | 'retain';
  /** Required when action='transfer' - the new owner's client/company id. */
  newOwnerType?: 'client' | 'company';
  newOwnerId?: string;
};

export type TenancyDecision = {
  tenancyId: string;
  action: 'cancel' | 'transfer';
  /** Required when action='transfer' - the new client id. */
  transferToClientId?: string;
};

export type InvoiceDecision = {
  invoiceId: string;
  action: 'void' | 'write_off' | 'leave';
};

export type DocumentDecision = {
  documentId: string;
  /** void = call Documenso API to void the envelope. leave = no action. */
  action: 'void_documenso' | 'leave';
};

export interface ArchiveDecisions {
  reason: string;
  /** Required acknowledgment when the dossier surfaces signed legal docs. */
  acknowledgedSignedDocuments: boolean;
  berthDecisions: BerthDecision[];
  yachtDecisions: YachtDecision[];
  tenancyDecisions: TenancyDecision[];
  invoiceDecisions: InvoiceDecision[];
  documentDecisions: DocumentDecision[];
}

// ─── Persisted decision log (lives in clients.archive_metadata jsonb) ───────

interface PersistedDecision {
  kind:
    | 'berth_released'
    | 'berth_retained'
    | 'yacht_transferred'
    | 'yacht_marked_sold_away'
    | 'yacht_retained'
    | 'tenancy_cancelled'
    | 'tenancy_transferred'
    | 'invoice_voided'
    | 'invoice_written_off'
    | 'invoice_left'
    | 'documenso_voided'
    | 'document_left'
    | 'portal_user_revoked';
  refId: string;
  detail?: Record<string, unknown>;
}

export interface ArchiveMetadata {
  decisions: PersistedDecision[];
  decidedAt: string;
  decidedBy: string;
  reason: string;
}

// ─── Result shape ───────────────────────────────────────────────────────────

export interface ArchiveResult {
  clientId: string;
  decisionsApplied: number;
  externalCleanups: Array<{
    kind: 'documenso_void';
    documentId: string;
    documensoId: string;
  }>;
  releasedBerths: Array<{
    berthId: string;
    mooringNumber: string;
    /** Other interests that should be notified about this berth becoming
     *  available - drives the "next in line" notification fire. */
    nextInLineInterestIds: string[];
  }>;
}

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

export async function archiveClientWithDecisions(args: {
  dossier: ClientArchiveDossier;
  decisions: ArchiveDecisions;
  meta: AuditMeta;
}): Promise<ArchiveResult> {
  const { dossier, decisions, meta } = args;
  const clientId = dossier.client.id;
  const portId = dossier.client.portId;

  // ─── Pre-checks (echo dossier blockers; UI can't bypass) ────────────────
  if (dossier.blockers.length > 0) {
    throw new ConflictError(
      `Cannot archive: ${dossier.blockers.length} unresolved blocker(s). ${dossier.blockers[0]}`,
    );
  }

  if (dossier.stakeLevel === 'high' && !decisions.reason.trim()) {
    throw new ValidationError(
      'A reason is required when archiving a client at Deposit Paid or later.',
    );
  }

  const hasSignedDocs = dossier.documents.some(
    (d) => d.status === 'completed' || d.status === 'signed',
  );
  if (hasSignedDocs && !decisions.acknowledgedSignedDocuments) {
    throw new ValidationError(
      'You must acknowledge that signed documents remain binding before archiving.',
    );
  }

  const persistedDecisions: PersistedDecision[] = [];
  const externalCleanups: ArchiveResult['externalCleanups'] = [];
  const releasedBerths: ArchiveResult['releasedBerths'] = [];

  // ─── Atomic local apply ──────────────────────────────────────────────────
  await db.transaction(async (tx) => {
    // Lock the client row so a concurrent archive collides cleanly.
    const [locked] = await tx
      .select({ id: clients.id, archivedAt: clients.archivedAt })
      .from(clients)
      .where(and(eq(clients.id, clientId), eq(clients.portId, portId)))
      .for('update');
    if (!locked) throw new NotFoundError('client');
    if (locked.archivedAt) throw new ConflictError('Client is already archived');

    // ─── Berth decisions ─────────────────────────────────────────────────
    for (const d of decisions.berthDecisions) {
      const berth = dossier.berths.find((b) => b.berthId === d.berthId);
      if (!berth) continue;
      if (d.action === 'release') {
        // Lock the berth row so a concurrent sale can't flip the status
        // between our read of dossier.berths (outside the tx) and our
        // write below. Without this lock, A archives client X while B
        // sells berth A1 to client Y - A's pre-tx read says
        // status='under_offer', B commits status='sold', A's update
        // would flip it back to 'available'.
        const [locked] = await tx
          .select({ status: berths.status })
          .from(berths)
          .where(eq(berths.id, d.berthId))
          .for('update');
        const lockedStatus = locked?.status ?? berth.status;

        // Drop the interest_berths link for this client's interest. Other
        // interests on the berth survive (so the next-in-line notification
        // can fire).
        await tx
          .delete(interestBerths)
          .where(
            and(eq(interestBerths.berthId, d.berthId), eq(interestBerths.interestId, d.interestId)),
          );
        // If no remaining interestBerths row marks this berth as
        // is_specific_interest, set the berth status back to available.
        // Sold berths are immutable from this flow - also re-checked
        // against the freshly-locked row, not the pre-tx dossier read.
        if (lockedStatus !== 'sold') {
          const [stillUnderOffer] = await tx
            .select({ count: sql<number>`count(*)::int` })
            .from(interestBerths)
            .innerJoin(interests, eq(interestBerths.interestId, interests.id))
            .where(
              and(
                activeInterestsWhere(portId),
                eq(interestBerths.berthId, d.berthId),
                eq(interestBerths.isSpecificInterest, true),
              ),
            );
          if ((stillUnderOffer?.count ?? 0) === 0) {
            await tx.update(berths).set({ status: 'available' }).where(eq(berths.id, d.berthId));
          }
        }
        persistedDecisions.push({
          kind: 'berth_released',
          refId: d.berthId,
          detail: { interestId: d.interestId, mooringNumber: berth.mooringNumber },
        });
        releasedBerths.push({
          berthId: d.berthId,
          mooringNumber: berth.mooringNumber,
          nextInLineInterestIds: berth.otherInterests.map((i) => i.interestId),
        });
      } else {
        persistedDecisions.push({
          kind: 'berth_retained',
          refId: d.berthId,
          detail: { interestId: d.interestId, mooringNumber: berth.mooringNumber },
        });
      }
    }

    // ─── Yacht decisions ─────────────────────────────────────────────────
    for (const d of decisions.yachtDecisions) {
      if (d.action === 'transfer') {
        if (!d.newOwnerType || !d.newOwnerId) {
          throw new ValidationError(
            `Yacht ${d.yachtId}: transfer requires newOwnerType + newOwnerId`,
          );
        }
        // Move ownership through the shared ledger-aware helper so the
        // yacht_ownership_history row is closed + reopened in lock-step
        // with the denormalized columns. Writing only the denorm columns
        // (the prior bug) left the history's open row pointing at the
        // archived client, corrupting the ledger on the next real transfer.
        await transferOwnershipTx(tx as unknown as typeof db, {
          yachtId: d.yachtId,
          newOwner: { type: d.newOwnerType, id: d.newOwnerId },
          transferReason: `Smart-archive: ${decisions.reason || 'client archived'}`,
          createdBy: meta.userId,
        });
        persistedDecisions.push({
          kind: 'yacht_transferred',
          refId: d.yachtId,
          detail: {
            previousOwnerType: 'client',
            previousOwnerId: clientId,
            newOwnerType: d.newOwnerType,
            newOwnerId: d.newOwnerId,
          },
        });
      } else if (d.action === 'mark_sold_away') {
        await tx.update(yachts).set({ status: 'sold_away' }).where(eq(yachts.id, d.yachtId));
        persistedDecisions.push({ kind: 'yacht_marked_sold_away', refId: d.yachtId });
      } else {
        persistedDecisions.push({ kind: 'yacht_retained', refId: d.yachtId });
      }
    }

    // ─── Tenancy decisions ───────────────────────────────────────────────
    for (const d of decisions.tenancyDecisions) {
      if (d.action === 'cancel') {
        await tx
          .update(berthTenancies)
          .set({ status: 'cancelled', updatedAt: new Date() })
          .where(eq(berthTenancies.id, d.tenancyId));
        persistedDecisions.push({ kind: 'tenancy_cancelled', refId: d.tenancyId });
      } else if (d.action === 'transfer') {
        if (!d.transferToClientId) {
          throw new ValidationError(`Tenancy ${d.tenancyId}: transfer requires transferToClientId`);
        }
        await tx
          .update(berthTenancies)
          .set({ clientId: d.transferToClientId, updatedAt: new Date() })
          .where(eq(berthTenancies.id, d.tenancyId));
        persistedDecisions.push({
          kind: 'tenancy_transferred',
          refId: d.tenancyId,
          detail: { previousClientId: clientId, newClientId: d.transferToClientId },
        });
      }
    }

    // ─── Invoice decisions ───────────────────────────────────────────────
    for (const d of decisions.invoiceDecisions) {
      if (d.action === 'void' || d.action === 'write_off') {
        await tx
          .update(invoices)
          .set({
            status: 'cancelled',
            notes: sql`coalesce(${invoices.notes}, '') || ${'\n[archive ' + new Date().toISOString() + '] ' + (d.action === 'void' ? 'voided' : 'written off') + ' as part of client archive'}`,
            updatedAt: new Date(),
          })
          .where(eq(invoices.id, d.invoiceId));
        persistedDecisions.push({
          kind: d.action === 'void' ? 'invoice_voided' : 'invoice_written_off',
          refId: d.invoiceId,
        });
      } else {
        persistedDecisions.push({ kind: 'invoice_left', refId: d.invoiceId });
      }
    }

    // ─── Document (Documenso envelope) decisions ─────────────────────────
    for (const d of decisions.documentDecisions) {
      const doc = dossier.documents.find((x) => x.documentId === d.documentId);
      if (!doc) continue;
      if (d.action === 'void_documenso' && doc.documensoEnvelopeId) {
        // Local marker - actual API call queued post-commit.
        await tx
          .update(documents)
          .set({ status: 'cancelled', updatedAt: new Date() })
          .where(eq(documents.id, d.documentId));
        externalCleanups.push({
          kind: 'documenso_void',
          documentId: d.documentId,
          documensoId: doc.documensoEnvelopeId,
        });
        persistedDecisions.push({
          kind: 'documenso_voided',
          refId: d.documentId,
          detail: { documensoEnvelopeId: doc.documensoEnvelopeId },
        });
      } else {
        persistedDecisions.push({ kind: 'document_left', refId: d.documentId });
      }
    }

    // ─── Auto-handled: portal user, company memberships ──────────────────
    if (dossier.hasPortalUser) {
      await tx
        .update(portalUsers)
        .set({ isActive: false, updatedAt: new Date() })
        .where(eq(portalUsers.clientId, clientId));
      persistedDecisions.push({ kind: 'portal_user_revoked', refId: clientId });
    }

    // Auto-end company memberships (no decision needed - preserves history
    // via end_date instead of deleting the membership row).
    await tx
      .update(companyMemberships)
      .set({ endDate: sql`now()` })
      .where(and(eq(companyMemberships.clientId, clientId), isNull(companyMemberships.endDate)));

    // ─── Archive the client itself ────────────────────────────────────────
    const archiveMetadata: ArchiveMetadata = {
      decisions: persistedDecisions,
      decidedAt: new Date().toISOString(),
      decidedBy: meta.userId,
      reason: decisions.reason,
    };
    await tx
      .update(clients)
      .set({
        archivedAt: new Date(),
        archivedBy: meta.userId,
        archiveReason: decisions.reason || null,
        archiveMetadata,
        updatedAt: new Date(),
      })
      .where(eq(clients.id, clientId));
  });

  // ─── Audit log (one parent + one per non-trivial decision) ──────────────
  void createAuditLog({
    portId,
    userId: meta.userId,
    action: 'archive',
    entityType: 'client',
    entityId: clientId,
    metadata: {
      stakeLevel: dossier.stakeLevel,
      highStakesStage: dossier.highStakesStage,
      reason: decisions.reason,
      decisionCount: persistedDecisions.length,
    },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  return {
    clientId,
    decisionsApplied: persistedDecisions.length,
    externalCleanups,
    releasedBerths,
  };
}

/** Re-export for convenience. */
export { HIGH_STAKES_STAGES };
