/**
 * Smart-restore service.
 *
 * Reads the persisted decision log from clients.archive_metadata and
 * classifies each decision as:
 *   - autoReversible     → safe to undo right now (system handles it
 *                          inside the restore tx).
 *   - reversibleWithPrompt → can be undone but the world has moved on a
 *                            bit; surfaces in the wizard with a checkbox
 *                            so the operator opts in.
 *   - locked             → can't be undone (a different client now owns
 *                          the resource, the berth is sold to someone
 *                          else, etc).
 *
 * Mutating restore happens in `restoreClientWithSelections` once the UI
 * has the operator's selections.
 */

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

import { db } from '@/lib/db';
import type { Tx } from '@/lib/db/utils';
import { clients } from '@/lib/db/schema/clients';
import { interests } from '@/lib/db/schema/interests';
import { berths } from '@/lib/db/schema/berths';
import { yachts } from '@/lib/db/schema/yachts';
import { portalUsers } from '@/lib/db/schema/portal';
import { documents } from '@/lib/db/schema/documents';
import { createAuditLog, type AuditMeta } from '@/lib/audit';
import { activeInterestsWhere } from '@/lib/services/active-interest';
import { transferOwnershipTx } from '@/lib/services/yachts.service';
import { upsertInterestBerthTx } from '@/lib/services/interest-berths.service';
import { ConflictError, NotFoundError } from '@/lib/errors';
import type { ArchiveMetadata } from '@/lib/services/client-archive.service';

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

export interface RestoreReversal {
  /** Stable id derived from the original decision so the UI can reference it. */
  id: string;
  /** Mirror of the original decision kind for UI rendering. */
  kind: ArchiveMetadata['decisions'][number]['kind'];
  /** Refers back to the entity that was changed (berth, yacht, etc). */
  refId: string;
  /** Human-readable label for the wizard ("Berth A12", "Yacht Schaefer 44"). */
  label: string;
  /** Why this is being shown the way it is (e.g. "berth still available"). */
  reason: string;
  /** Carries the persisted decision detail through to applyReversal so we
   *  can re-link berths to their original interest, restore yacht owners,
   *  etc. without re-parsing meta.decisions. */
  detail?: Record<string, unknown>;
}

export interface RestoreDossier {
  client: { id: string; fullName: string; portId: string };
  /** Always reversed automatically inside the restore transaction. */
  autoReversible: RestoreReversal[];
  /** Surfaces as opt-in checkboxes in the wizard. */
  reversibleWithPrompt: RestoreReversal[];
  /** Read-only list explaining what won't be restored. */
  locked: Array<RestoreReversal & { lockReason: string }>;
}

export interface RestoreSelections {
  /** ids from RestoreDossier.reversibleWithPrompt the operator opted into. */
  applyReversals: string[];
}

export interface RestoreResult {
  clientId: string;
  autoReversed: number;
  promptedReversed: number;
  lockedSkipped: number;
}

// ─── Dossier ────────────────────────────────────────────────────────────────

export async function getRestoreDossier(clientId: string, portId: string): Promise<RestoreDossier> {
  const [client] = await db
    .select({
      id: clients.id,
      fullName: clients.fullName,
      portId: clients.portId,
      archivedAt: clients.archivedAt,
      archiveMetadata: clients.archiveMetadata,
    })
    .from(clients)
    .where(and(eq(clients.id, clientId), eq(clients.portId, portId)))
    .limit(1);

  if (!client) throw new NotFoundError('client');
  if (!client.archivedAt) throw new ConflictError('client is not archived');

  const auto: RestoreReversal[] = [];
  const prompt: RestoreReversal[] = [];
  const locked: Array<RestoreReversal & { lockReason: string }> = [];

  const meta = (client.archiveMetadata ?? null) as ArchiveMetadata | null;
  if (!meta || !meta.decisions || meta.decisions.length === 0) {
    return {
      client: { id: client.id, fullName: client.fullName, portId: client.portId },
      autoReversible: [],
      reversibleWithPrompt: [],
      locked: [],
    };
  }

  for (const d of meta.decisions) {
    switch (d.kind) {
      case 'berth_released': {
        // Try to re-attach: only safe if the berth still exists and is
        // still 'available' (i.e. nobody has snapped it up since).
        const [b] = await db
          .select({ id: berths.id, mooringNumber: berths.mooringNumber, status: berths.status })
          .from(berths)
          .where(eq(berths.id, d.refId))
          .limit(1);
        if (!b) {
          locked.push({
            id: `berth-${d.refId}`,
            kind: d.kind,
            refId: d.refId,
            label: `Berth ${(d.detail?.mooringNumber as string) ?? d.refId.slice(0, 8)}`,
            reason: 'released to available during archive',
            lockReason: 'berth no longer exists',
          });
          break;
        }
        if (b.status === 'available') {
          auto.push({
            id: `berth-${d.refId}`,
            kind: d.kind,
            refId: d.refId,
            label: `Berth ${b.mooringNumber}`,
            reason: 'still available - re-attaching to the restored client',
            detail: d.detail,
          });
        } else if (b.status === 'sold') {
          locked.push({
            id: `berth-${d.refId}`,
            kind: d.kind,
            refId: d.refId,
            label: `Berth ${b.mooringNumber}`,
            reason: 'released during archive',
            lockReason: 'berth has since been sold to another client',
          });
        } else {
          // under_offer to a different interest now
          prompt.push({
            id: `berth-${d.refId}`,
            kind: d.kind,
            refId: d.refId,
            label: `Berth ${b.mooringNumber}`,
            reason: 'currently under offer to another client - re-attach as a competing interest?',
            detail: d.detail,
          });
        }
        break;
      }
      case 'yacht_transferred': {
        const [y] = await db
          .select({
            id: yachts.id,
            name: yachts.name,
            currentOwnerType: yachts.currentOwnerType,
            currentOwnerId: yachts.currentOwnerId,
          })
          .from(yachts)
          .where(eq(yachts.id, d.refId))
          .limit(1);
        if (!y) {
          locked.push({
            id: `yacht-${d.refId}`,
            kind: d.kind,
            refId: d.refId,
            label: 'Yacht',
            reason: 'transferred during archive',
            lockReason: 'yacht no longer exists',
          });
          break;
        }
        // Look for active interests on the new owner that USE this yacht -
        // if any exist, the new owner's deal depends on the yacht and we
        // shouldn't yank ownership back without their consent.
        const [usage] = await db
          .select({ count: sql<number>`count(*)::int` })
          .from(interests)
          .where(
            and(
              activeInterestsWhere(portId),
              eq(interests.yachtId, y.id),
              ne(interests.clientId, clientId),
            ),
          );
        if ((usage?.count ?? 0) > 0) {
          locked.push({
            id: `yacht-${d.refId}`,
            kind: d.kind,
            refId: d.refId,
            label: `Yacht ${y.name}`,
            reason: 'transferred during archive',
            lockReason: 'new owner has active interests using this yacht',
          });
        } else {
          prompt.push({
            id: `yacht-${d.refId}`,
            kind: d.kind,
            refId: d.refId,
            label: `Yacht ${y.name}`,
            reason:
              'currently owned by another party with no active dependent interests - transfer back?',
          });
        }
        break;
      }
      case 'yacht_marked_sold_away':
      case 'yacht_retained': {
        // Sold-away is a label change; restore can flip it back to active
        // automatically. Retained never moved, no action needed.
        if (d.kind === 'yacht_marked_sold_away') {
          auto.push({
            id: `yacht-status-${d.refId}`,
            kind: d.kind,
            refId: d.refId,
            label: 'Yacht status',
            reason: 'was marked sold-away during archive - restoring to active',
          });
        }
        break;
      }
      case 'portal_user_revoked': {
        const [pu] = await db
          .select({ id: portalUsers.id, isActive: portalUsers.isActive })
          .from(portalUsers)
          .where(eq(portalUsers.clientId, clientId))
          .limit(1);
        if (pu && !pu.isActive) {
          auto.push({
            id: `portal-${pu.id}`,
            kind: d.kind,
            refId: pu.id,
            label: 'Portal user account',
            reason: 'was deactivated during archive - restoring access',
          });
        }
        break;
      }
      case 'documenso_voided': {
        // Already void in Documenso; we can't un-void. Inform the operator.
        locked.push({
          id: `doc-${d.refId}`,
          kind: d.kind,
          refId: d.refId,
          label: 'Documenso envelope',
          reason: 'voided during archive',
          lockReason: 'voided envelopes cannot be re-opened - regenerate the EOI if needed',
        });
        break;
      }
      case 'invoice_voided':
      case 'invoice_written_off':
        locked.push({
          id: `invoice-${d.refId}`,
          kind: d.kind,
          refId: d.refId,
          label: 'Invoice',
          reason:
            d.kind === 'invoice_voided' ? 'voided during archive' : 'written off during archive',
          lockReason:
            'invoice status changes are not reversed by restore - un-cancel manually if needed',
        });
        break;
      // Berth retained, yacht retained, document left, invoice left,
      // reservation_* - no action surfaced because nothing changed.
      default:
        break;
    }
  }

  return {
    client: { id: client.id, fullName: client.fullName, portId: client.portId },
    autoReversible: auto,
    reversibleWithPrompt: prompt,
    locked,
  };
}

// ─── Mutating restore ────────────────────────────────────────────────────────

export async function restoreClientWithSelections(args: {
  clientId: string;
  portId: string;
  selections: RestoreSelections;
  meta: AuditMeta;
}): Promise<RestoreResult> {
  const dossier = await getRestoreDossier(args.clientId, args.portId);
  const opted = new Set(args.selections.applyReversals);

  let autoReversed = 0;
  let promptedReversed = 0;

  await db.transaction(async (tx) => {
    // Lock the client to prevent concurrent restore.
    const [locked] = await tx
      .select({ id: clients.id, archivedAt: clients.archivedAt })
      .from(clients)
      .where(and(eq(clients.id, args.clientId), eq(clients.portId, args.portId)))
      .for('update');
    if (!locked) throw new NotFoundError('client');
    if (!locked.archivedAt) throw new ConflictError('client is not archived');

    // Apply auto-reversals.
    for (const r of dossier.autoReversible) {
      await applyReversal(tx, r, args.clientId, args.meta.userId);
      autoReversed += 1;
    }

    // Apply opted-in prompts.
    for (const r of dossier.reversibleWithPrompt) {
      if (!opted.has(r.id)) continue;
      await applyReversal(tx, r, args.clientId, args.meta.userId);
      promptedReversed += 1;
    }

    // Restore the client itself.
    await tx
      .update(clients)
      .set({
        archivedAt: null,
        archivedBy: null,
        archiveReason: null,
        archiveMetadata: null,
        updatedAt: new Date(),
      })
      .where(eq(clients.id, args.clientId));
  });

  void createAuditLog({
    portId: args.portId,
    userId: args.meta.userId,
    action: 'restore',
    entityType: 'client',
    entityId: args.clientId,
    metadata: {
      autoReversed,
      promptedReversed,
      lockedSkipped: dossier.locked.length,
    },
    ipAddress: args.meta.ipAddress,
    userAgent: args.meta.userAgent,
  });

  return {
    clientId: args.clientId,
    autoReversed,
    promptedReversed,
    lockedSkipped: dossier.locked.length,
  };
}

async function applyReversal(
  tx: Tx,
  r: RestoreReversal,
  clientId: string,
  actorUserId: string,
): Promise<void> {
  switch (r.kind) {
    case 'berth_released': {
      // Re-link the berth to whichever interest originally owned it
      // (persisted in d.detail.interestId at archive time). We verify
      // the interest still belongs to the restored client and isn't
      // archived - defensive in case the operator deleted the interest
      // separately while the client was archived.
      const interestId = (r.detail?.interestId as string | undefined) ?? null;
      if (!interestId) break;
      const [iv] = await tx
        .select({ id: interests.id, archivedAt: interests.archivedAt })
        .from(interests)
        .where(and(eq(interests.id, interestId), eq(interests.clientId, clientId)))
        .limit(1);
      if (!iv || iv.archivedAt) break;

      // Idempotent re-link via the canonical junction helper (audit L27):
      // routes through `upsertInterestBerthTx` so the cross-port guard runs
      // (the prior raw insert bypassed it) and the unique (interestId, berthId)
      // index keeps a duplicate a benign merge. This row is a non-primary
      // re-attach, so the primary↔bundle invariant doesn't force the bundle
      // flag on — it stays an EOI-only/legal link as before.
      await upsertInterestBerthTx(tx, interestId, r.refId, {
        isPrimary: false,
        isSpecificInterest: true,
        isInEoiBundle: false,
      });
      // Flip berth status back to under_offer so the public map reflects
      // the re-link. Only when berth is currently 'available' (sold
      // berths are immutable; under_offer to another client is handled
      // via the prompt branch which the operator may opt into).
      await tx
        .update(berths)
        .set({ status: 'under_offer' })
        .where(and(eq(berths.id, r.refId), eq(berths.status, 'available')));
      break;
    }
    case 'yacht_transferred': {
      // Transfer back to the restored client through the shared
      // ledger-aware helper: closes the open yacht_ownership_history row
      // (whoever the archive transfer pointed it at) and opens a fresh one
      // for the restored client, keeping the ledger and denorm columns in
      // sync. Writing only the denorm columns (the prior bug) re-corrupted
      // the history on every restore.
      await transferOwnershipTx(tx as unknown as typeof db, {
        yachtId: r.refId,
        newOwner: { type: 'client', id: clientId },
        transferReason: 'Smart-restore: ownership returned to restored client',
        createdBy: actorUserId,
      });
      break;
    }
    case 'yacht_marked_sold_away':
      await tx.update(yachts).set({ status: 'active' }).where(eq(yachts.id, r.refId));
      break;
    case 'portal_user_revoked':
      await tx
        .update(portalUsers)
        .set({ isActive: true, updatedAt: new Date() })
        .where(eq(portalUsers.id, r.refId));
      break;
    default:
      break;
  }
}

// Suppress lint for the test-helper imports used by future integration tests.
void documents;
