import { and, desc, eq, inArray, isNull, ne } from 'drizzle-orm';
import { db } from '@/lib/db';
import { companies, companyMemberships } from '@/lib/db/schema/companies';
import type { CompanyMembership } from '@/lib/db/schema/companies';
import { clients } from '@/lib/db/schema/clients';
import { withTransaction } from '@/lib/db/utils';
import { createAuditLog, toAuditJson, type AuditMeta } from '@/lib/audit';
import { NotFoundError, ConflictError, ValidationError } from '@/lib/errors';
import { emitToRoom } from '@/lib/socket/server';
import { diffEntity } from '@/lib/entity-diff';
import type {
  AddMembershipInput,
  UpdateMembershipInput,
  EndMembershipInput,
} from '@/lib/validators/company-memberships';

export type { CompanyMembership };

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

/**
 * Returns true if the error is a Postgres unique-violation (SQLSTATE 23505).
 */
function isUniqueViolation(err: unknown): boolean {
  if (!err || typeof err !== 'object') return false;
  const e = err as { code?: unknown; cause?: { code?: unknown } };
  if (e.code === '23505') return true;
  if (e.cause && typeof e.cause === 'object' && e.cause.code === '23505') return true;
  return false;
}

/**
 * Loads a membership row and verifies the joined company belongs to `portId`.
 * Throws NotFoundError('Membership') if the row is missing or cross-tenant.
 *
 * Uses a JOIN to companies (memberships have no portId column - they inherit
 * tenancy via the parent company).
 */
async function loadMembershipScoped(
  membershipId: string,
  portId: string,
): Promise<CompanyMembership> {
  const rows = await db
    .select({ membership: companyMemberships })
    .from(companyMemberships)
    .innerJoin(companies, eq(companies.id, companyMemberships.companyId))
    .where(and(eq(companyMemberships.id, membershipId), eq(companies.portId, portId)))
    .limit(1);

  const row = rows[0];
  if (!row) throw new NotFoundError('Membership');
  return row.membership;
}

// ─── Add ─────────────────────────────────────────────────────────────────────

export async function addMembership(
  companyId: string,
  portId: string,
  data: AddMembershipInput,
  meta: AuditMeta,
): Promise<CompanyMembership> {
  // Verify the company exists in this port.
  const company = await db.query.companies.findFirst({
    where: and(eq(companies.id, companyId), eq(companies.portId, portId)),
  });
  if (!company) throw new ValidationError('company not found');

  // Verify the client exists in this port.
  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');

  try {
    const [membership] = await db
      .insert(companyMemberships)
      .values({
        companyId,
        clientId: data.clientId,
        role: data.role,
        roleDetail: data.roleDetail ?? null,
        startDate: data.startDate,
        isPrimary: data.isPrimary ?? false,
        notes: data.notes ?? null,
      })
      .returning();

    void createAuditLog({
      userId: meta.userId,
      portId,
      action: 'create',
      entityType: 'company_membership',
      entityId: membership!.id,
      newValue: {
        companyId: membership!.companyId,
        clientId: membership!.clientId,
        role: membership!.role,
        startDate: membership!.startDate,
      },
      ipAddress: meta.ipAddress,
      userAgent: meta.userAgent,
    });

    emitToRoom(`port:${portId}`, 'company_membership:added', {
      membershipId: membership!.id,
      companyId: membership!.companyId,
      clientId: membership!.clientId,
    });

    return membership!;
  } catch (err) {
    if (isUniqueViolation(err)) {
      throw new ConflictError('membership already exists');
    }
    throw err;
  }
}

// ─── Update ──────────────────────────────────────────────────────────────────

export async function updateMembership(
  membershipId: string,
  portId: string,
  data: UpdateMembershipInput,
  meta: AuditMeta,
): Promise<CompanyMembership> {
  const existing = await loadMembershipScoped(membershipId, portId);

  const { diff } = diffEntity(toAuditJson(existing), data as Record<string, unknown>);

  const rows = await db
    .update(companyMemberships)
    .set({ ...data, updatedAt: new Date() })
    // Defense-in-depth: companyMemberships has no port_id column, so the
    // tenant boundary lives on the parent company. Pin the WHERE to a
    // sub-select of port companies - even if loadMembershipScoped were
    // ever bypassed, a stray membershipId from another tenant cannot
    // mutate.
    .where(
      and(
        eq(companyMemberships.id, membershipId),
        inArray(
          companyMemberships.companyId,
          db.select({ id: companies.id }).from(companies).where(eq(companies.portId, portId)),
        ),
      ),
    )
    .returning();

  const updated = rows[0]!;

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'update',
    entityType: 'company_membership',
    entityId: membershipId,
    oldValue: diff as Record<string, unknown>,
    newValue: data as Record<string, unknown>,
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  emitToRoom(`port:${portId}`, 'company_membership:updated', {
    membershipId,
    changedFields: Object.keys(diff),
  });

  return updated;
}

// ─── End (set endDate) ───────────────────────────────────────────────────────

export async function endMembership(
  membershipId: string,
  portId: string,
  data: EndMembershipInput,
  meta: AuditMeta,
): Promise<CompanyMembership> {
  const existing = await loadMembershipScoped(membershipId, portId);

  const rows = await db
    .update(companyMemberships)
    .set({ endDate: data.endDate, updatedAt: new Date() })
    // Defense-in-depth: companyMemberships has no port_id column, so the
    // tenant boundary lives on the parent company. Pin the WHERE to a
    // sub-select of port companies - even if loadMembershipScoped were
    // ever bypassed, a stray membershipId from another tenant cannot
    // mutate.
    .where(
      and(
        eq(companyMemberships.id, membershipId),
        inArray(
          companyMemberships.companyId,
          db.select({ id: companies.id }).from(companies).where(eq(companies.portId, portId)),
        ),
      ),
    )
    .returning();

  const updated = rows[0]!;

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

  emitToRoom(`port:${portId}`, 'company_membership:ended', {
    membershipId,
    companyId: updated.companyId,
    clientId: updated.clientId,
  });

  return updated;
}

// ─── Set Primary (atomic) ────────────────────────────────────────────────────

/**
 * Marks this membership as primary for its company, un-primary-ing any other
 * memberships of the same company atomically.
 */
export async function setPrimary(
  membershipId: string,
  portId: string,
  meta: AuditMeta,
): Promise<CompanyMembership> {
  // Tenant-scoped load (outside tx is fine - we re-read inside).
  const existing = await loadMembershipScoped(membershipId, portId);

  return await withTransaction(async (tx) => {
    await tx
      .update(companyMemberships)
      .set({ isPrimary: false, updatedAt: new Date() })
      .where(
        and(
          eq(companyMemberships.companyId, existing.companyId),
          ne(companyMemberships.id, membershipId),
        ),
      );

    const rows = await tx
      .update(companyMemberships)
      .set({ isPrimary: true, updatedAt: new Date() })
      // Same defense-in-depth via port-companies sub-select - see updateMembership.
      .where(
        and(
          eq(companyMemberships.id, membershipId),
          inArray(
            companyMemberships.companyId,
            tx.select({ id: companies.id }).from(companies).where(eq(companies.portId, portId)),
          ),
        ),
      )
      .returning();

    const updated = rows[0]!;

    void createAuditLog({
      userId: meta.userId,
      portId,
      action: 'update',
      entityType: 'company_membership',
      entityId: membershipId,
      oldValue: { isPrimary: existing.isPrimary },
      newValue: { isPrimary: true },
      ipAddress: meta.ipAddress,
      userAgent: meta.userAgent,
    });

    emitToRoom(`port:${portId}`, 'company_membership:updated', {
      membershipId,
      changedFields: ['isPrimary'],
    });

    return updated;
  });
}

// ─── List by Company ─────────────────────────────────────────────────────────

export async function listByCompany(
  companyId: string,
  portId: string,
  opts?: { activeOnly?: boolean },
): Promise<CompanyMembership[]> {
  const activeOnly = opts?.activeOnly ?? true;

  // Verify the company belongs to the port (prevents cross-tenant enumeration).
  const company = await db.query.companies.findFirst({
    where: and(eq(companies.id, companyId), eq(companies.portId, portId)),
  });
  if (!company) throw new NotFoundError('Company');

  const conditions = [eq(companyMemberships.companyId, companyId)];
  if (activeOnly) conditions.push(isNull(companyMemberships.endDate));

  return await db
    .select()
    .from(companyMemberships)
    .where(and(...conditions))
    .orderBy(desc(companyMemberships.isPrimary), desc(companyMemberships.startDate));
}

// ─── List by Client ──────────────────────────────────────────────────────────

export async function listByClient(
  clientId: string,
  portId: string,
  opts?: { activeOnly?: boolean },
): Promise<CompanyMembership[]> {
  const activeOnly = opts?.activeOnly ?? true;

  // Verify the client belongs to the port.
  const client = await db.query.clients.findFirst({
    where: and(eq(clients.id, clientId), eq(clients.portId, portId)),
  });
  if (!client) throw new NotFoundError('Client');

  const conditions = [eq(companyMemberships.clientId, clientId)];
  if (activeOnly) conditions.push(isNull(companyMemberships.endDate));

  return await db
    .select()
    .from(companyMemberships)
    .where(and(...conditions))
    .orderBy(desc(companyMemberships.isPrimary), desc(companyMemberships.startDate));
}
