import { and, eq, isNull } from 'drizzle-orm';
import { db } from '@/lib/db';
import { berthReservations, type BerthReservation } from '@/lib/db/schema/reservations';
import { berths } from '@/lib/db/schema/berths';
import { clients } from '@/lib/db/schema/clients';
import { files } from '@/lib/db/schema/documents';
import { yachts } from '@/lib/db/schema/yachts';
import { companyMemberships } from '@/lib/db/schema/companies';
import { buildListQuery } from '@/lib/db/query-builder';
import { createAuditLog, type AuditMeta } from '@/lib/audit';
import { ConflictError, NotFoundError, ValidationError } from '@/lib/errors';
import { emitToRoom } from '@/lib/socket/server';
import type { z } from 'zod';
import { createPendingSchema } from '@/lib/validators/reservations';
import type {
  ActivateInput,
  EndReservationInput,
  CancelInput,
  ListReservationsInput,
} from '@/lib/validators/reservations';

// Use z.input so callers (including tests) can omit fields with
// `.default()` like `tenureType`. The service re-parses below to get
// the post-coercion shape Drizzle expects (Date, defaulted tenureType).
type CreatePendingInput = z.input<typeof createPendingSchema>;

export type { BerthReservation };

// ─── Helpers ─────────────────────────────────────────────────────────────────

/**
 * Returns true if the error is a Postgres unique-violation (SQLSTATE 23505)
 * raised specifically on the `idx_br_active` partial unique index. Narrowing
 * to this constraint name prevents us from swallowing unrelated unique
 * violations.
 */
function isBerthActiveConflict(err: unknown): boolean {
  if (!err || typeof err !== 'object') return false;
  const e = err as {
    code?: unknown;
    constraint_name?: unknown;
    constraint?: unknown;
    cause?: { code?: unknown; constraint_name?: unknown; constraint?: unknown };
  };
  const code = e.code ?? e.cause?.code;
  if (code !== '23505') return false;
  const constraint =
    e.constraint_name ?? e.constraint ?? e.cause?.constraint_name ?? e.cause?.constraint;
  return constraint === 'idx_br_active';
}

/**
 * Cross-references the reservation's client against the yacht's current owner.
 * Either the yacht is directly owned by the client, OR the client has an
 * active (endDate IS NULL) company_membership on the owning company.
 */
async function assertClientOwnsOrRepresentsYacht(
  yacht: { currentOwnerType: string; currentOwnerId: string },
  clientId: string,
): Promise<void> {
  if (yacht.currentOwnerType === 'client' && yacht.currentOwnerId === clientId) {
    return;
  }

  if (yacht.currentOwnerType === 'company') {
    const membership = await db.query.companyMemberships.findFirst({
      where: and(
        eq(companyMemberships.companyId, yacht.currentOwnerId),
        eq(companyMemberships.clientId, clientId),
        isNull(companyMemberships.endDate),
      ),
    });
    if (membership) return;
  }

  throw new ValidationError('yacht does not belong to reservation client');
}

async function loadScoped(id: string, portId: string): Promise<BerthReservation> {
  const row = await db.query.berthReservations.findFirst({
    where: and(eq(berthReservations.id, id), eq(berthReservations.portId, portId)),
  });
  if (!row) throw new NotFoundError('Reservation');
  return row;
}

// ─── Create (pending) ────────────────────────────────────────────────────────

export async function createPending(
  portId: string,
  data: CreatePendingInput,
  meta: AuditMeta,
): Promise<BerthReservation> {
  // Tenant-scoped existence checks (berth, client, yacht).
  const berth = await db.query.berths.findFirst({
    where: and(eq(berths.id, data.berthId), eq(berths.portId, portId)),
  });
  if (!berth) throw new ValidationError('berth not found');

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

  const yacht = await db.query.yachts.findFirst({
    where: and(eq(yachts.id, data.yachtId), eq(yachts.portId, portId)),
  });
  if (!yacht) throw new ValidationError('yacht not found');

  // Client must own the yacht directly OR be an active member of the owning company.
  await assertClientOwnsOrRepresentsYacht(
    { currentOwnerType: yacht.currentOwnerType, currentOwnerId: yacht.currentOwnerId },
    data.clientId,
  );

  // Re-parse to apply coercions/defaults locally — Drizzle's .values()
  // wants the post-coercion shape (Date, defaulted enum), and v4's
  // z.input is too loose to satisfy that.
  const parsed = createPendingSchema.parse(data);

  const [reservation] = await db
    .insert(berthReservations)
    .values({
      portId,
      berthId: parsed.berthId,
      clientId: parsed.clientId,
      yachtId: parsed.yachtId,
      interestId: parsed.interestId ?? null,
      status: 'pending',
      startDate: parsed.startDate,
      tenureType: parsed.tenureType,
      notes: parsed.notes ?? null,
      createdBy: meta.userId,
    })
    .returning();

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'create',
    entityType: 'berth_reservation',
    entityId: reservation!.id,
    newValue: {
      berthId: reservation!.berthId,
      clientId: reservation!.clientId,
      yachtId: reservation!.yachtId,
      status: reservation!.status,
      startDate: reservation!.startDate,
    },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  emitToRoom(`port:${portId}`, 'berth_reservation:created', {
    reservationId: reservation!.id,
    berthId: reservation!.berthId,
  });

  return reservation!;
}

// ─── Activate (pending → active) ─────────────────────────────────────────────

export async function activate(
  reservationId: string,
  portId: string,
  data: ActivateInput,
  meta: AuditMeta,
): Promise<BerthReservation> {
  const existing = await loadScoped(reservationId, portId);

  if (existing.status !== 'pending') {
    throw new ValidationError(`invalid transition: ${existing.status} → active`);
  }

  const patch: Partial<typeof berthReservations.$inferInsert> = {
    status: 'active',
    updatedAt: new Date(),
  };
  if (data.contractFileId !== undefined) {
    // Verify the contract file lives in the caller's port. Without this,
    // a port-A user activating a port-A reservation could attach a
    // port-B file id and downstream presigned-download paths (admin
    // export, portal contract download) would otherwise leak that
    // foreign-port content.
    if (data.contractFileId !== null) {
      const contractFile = await db.query.files.findFirst({
        where: and(eq(files.id, data.contractFileId), eq(files.portId, portId)),
      });
      if (!contractFile) {
        throw new ValidationError('contract file not found in this port');
      }
    }
    patch.contractFileId = data.contractFileId;
  }
  if (data.effectiveDate !== undefined) {
    patch.startDate = data.effectiveDate;
  }

  let updated: BerthReservation | undefined;
  try {
    const rows = await db
      .update(berthReservations)
      .set(patch)
      .where(and(eq(berthReservations.id, reservationId), eq(berthReservations.portId, portId)))
      .returning();
    updated = rows[0];
  } catch (err) {
    if (isBerthActiveConflict(err)) {
      const conflicting = await db.query.berthReservations.findFirst({
        where: and(
          eq(berthReservations.berthId, existing.berthId),
          eq(berthReservations.status, 'active'),
          eq(berthReservations.portId, portId),
        ),
      });
      throw new ConflictError(
        conflicting
          ? `berth already has active reservation (conflictingReservationId: ${conflicting.id})`
          : 'berth already has active reservation',
      );
    }
    throw err;
  }

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'update',
    entityType: 'berth_reservation',
    entityId: reservationId,
    oldValue: { status: existing.status },
    newValue: { status: 'active', contractFileId: updated!.contractFileId },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  emitToRoom(`port:${portId}`, 'berth_reservation:activated', {
    reservationId,
    berthId: updated!.berthId,
  });

  return updated!;
}

// ─── End (active → ended) ────────────────────────────────────────────────────

export async function endReservation(
  reservationId: string,
  portId: string,
  data: EndReservationInput,
  meta: AuditMeta,
): Promise<BerthReservation> {
  const existing = await loadScoped(reservationId, portId);

  if (existing.status !== 'active') {
    throw new ValidationError(`invalid transition: ${existing.status} → ended`);
  }

  const rows = await db
    .update(berthReservations)
    .set({
      status: 'ended',
      endDate: data.endDate,
      notes: data.notes ?? existing.notes,
      updatedAt: new Date(),
    })
    .where(and(eq(berthReservations.id, reservationId), eq(berthReservations.portId, portId)))
    .returning();

  const updated = rows[0]!;

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'update',
    entityType: 'berth_reservation',
    entityId: reservationId,
    oldValue: { status: existing.status },
    newValue: { status: 'ended', endDate: updated.endDate },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  emitToRoom(`port:${portId}`, 'berth_reservation:ended', {
    reservationId,
    berthId: updated.berthId,
  });

  return updated;
}

// ─── Cancel (pending|active → cancelled) ─────────────────────────────────────

export async function cancel(
  reservationId: string,
  portId: string,
  data: CancelInput,
  meta: AuditMeta,
): Promise<BerthReservation> {
  const existing = await loadScoped(reservationId, portId);

  if (existing.status !== 'pending' && existing.status !== 'active') {
    throw new ValidationError(`invalid transition: ${existing.status} → cancelled`);
  }

  const rows = await db
    .update(berthReservations)
    .set({
      status: 'cancelled',
      notes: data.reason
        ? `${existing.notes ? `${existing.notes}\n` : ''}Cancelled: ${data.reason}`
        : existing.notes,
      updatedAt: new Date(),
    })
    .where(and(eq(berthReservations.id, reservationId), eq(berthReservations.portId, portId)))
    .returning();

  const updated = rows[0]!;

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'update',
    entityType: 'berth_reservation',
    entityId: reservationId,
    oldValue: { status: existing.status },
    newValue: { status: 'cancelled', reason: data.reason ?? null },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  emitToRoom(`port:${portId}`, 'berth_reservation:cancelled', {
    reservationId,
    berthId: updated.berthId,
  });

  return updated;
}

// ─── Get ─────────────────────────────────────────────────────────────────────

export async function getById(id: string, portId: string): Promise<BerthReservation> {
  return loadScoped(id, portId);
}

// ─── List ────────────────────────────────────────────────────────────────────

export async function listReservations(
  portId: string,
  query: ListReservationsInput,
): Promise<{ data: BerthReservation[]; total: number }> {
  const { page, limit, sort, order, search, status, berthId, clientId, yachtId } = query;

  const filters = [];
  if (status) filters.push(eq(berthReservations.status, status));
  if (berthId) filters.push(eq(berthReservations.berthId, berthId));
  if (clientId) filters.push(eq(berthReservations.clientId, clientId));
  if (yachtId) filters.push(eq(berthReservations.yachtId, yachtId));

  let sortColumn:
    | typeof berthReservations.startDate
    | typeof berthReservations.createdAt
    | typeof berthReservations.updatedAt = berthReservations.updatedAt;
  if (sort === 'startDate') sortColumn = berthReservations.startDate;
  else if (sort === 'createdAt') sortColumn = berthReservations.createdAt;

  const result = await buildListQuery<BerthReservation>({
    table: berthReservations,
    portIdColumn: berthReservations.portId,
    portId,
    idColumn: berthReservations.id,
    updatedAtColumn: berthReservations.updatedAt,
    searchColumns: search ? [berthReservations.notes] : [],
    searchTerm: search,
    filters,
    sort: sort ? { column: sortColumn, direction: order } : undefined,
    page,
    pageSize: limit,
    includeArchived: true,
  });

  return result;
}
