import { and, count, desc, eq, ilike, inArray, isNull, or, sql } from 'drizzle-orm';
import { db } from '@/lib/db';
import {
  companies,
  companyMemberships,
  companyTags,
  companyAddresses,
} from '@/lib/db/schema/companies';
import type { Company } from '@/lib/db/schema/companies';
import { yachts } from '@/lib/db/schema/yachts';
import { withTransaction } from '@/lib/db/utils';
import { buildListQuery } from '@/lib/db/query-builder';
import { createAuditLog, toAuditJson, type AuditMeta } from '@/lib/audit';
import { NotFoundError, ConflictError } from '@/lib/errors';
import { logger } from '@/lib/logger';
import {
  syncEntityFolderName,
  applyEntityArchivedSuffix,
} from '@/lib/services/document-folders.service';
import { emitToRoom } from '@/lib/socket/server';
import { setEntityTags } from '@/lib/services/entity-tags.helper';
import { diffEntity } from '@/lib/entity-diff';
import type { z } from 'zod';
import type {
  createCompanySchema,
  UpdateCompanyInput,
  ListCompaniesInput,
} from '@/lib/validators/companies';

type CreateCompanyInput = z.output<typeof createCompanySchema>;

export type { Company };

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

/**
 * Returns true if the error is a Postgres unique-violation (SQLSTATE 23505).
 * We check a few shapes because the exact object depends on the driver.
 */
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;
}

// ─── Create ──────────────────────────────────────────────────────────────────

export async function createCompany(portId: string, data: CreateCompanyInput, meta: AuditMeta) {
  // Pre-check (case-insensitive) for friendlier ConflictError; the partial unique
  // index `idx_companies_name_unique ON companies(portId, lower(name))` is the
  // authoritative guard and caught below as defense-in-depth.
  const existing = await db.query.companies.findFirst({
    where: and(eq(companies.portId, portId), sql`lower(${companies.name}) = lower(${data.name})`),
  });
  if (existing) {
    throw new ConflictError('company name already exists');
  }

  try {
    return await withTransaction(async (tx) => {
      const [company] = await tx
        .insert(companies)
        .values({
          portId,
          name: data.name,
          legalName: data.legalName ?? null,
          taxId: data.taxId ?? null,
          registrationNumber: data.registrationNumber ?? null,
          incorporationCountryIso: data.incorporationCountryIso ?? null,
          incorporationSubdivisionIso: data.incorporationSubdivisionIso ?? null,
          incorporationDate: data.incorporationDate ?? null,
          status: data.status ?? 'active',
          billingEmail: data.billingEmail ?? null,
          notes: data.notes ?? null,
        })
        .returning();

      const tagIds = data.tagIds ?? [];
      if (tagIds.length > 0) {
        await tx
          .insert(companyTags)
          .values(tagIds.map((tagId) => ({ companyId: company!.id, tagId })));
      }

      void createAuditLog({
        userId: meta.userId,
        portId,
        action: 'create',
        entityType: 'company',
        entityId: company!.id,
        newValue: { name: company!.name, status: company!.status },
        ipAddress: meta.ipAddress,
        userAgent: meta.userAgent,
      });

      emitToRoom(`port:${portId}`, 'company:created', { companyId: company!.id });

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

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

export async function getCompanyById(id: string, portId: string) {
  const company = await db.query.companies.findFirst({
    where: and(eq(companies.id, id), eq(companies.portId, portId)),
    with: {
      tags: { with: { tag: true } },
    },
  });
  if (!company) throw new NotFoundError('Company');
  const { tags: tagJoins, ...rest } = company as typeof company & {
    tags: Array<{ tag: { id: string; name: string; color: string } }>;
  };

  const addresses = await db.query.companyAddresses.findMany({
    where: eq(companyAddresses.companyId, id),
    orderBy: (t, { desc }) => [desc(t.isPrimary), desc(t.createdAt)],
  });

  // Aggregated note count for the Notes tab badge. Symmetric-reach via
  // owned yachts + their linked interests (member-client personal
  // notes intentionally excluded — they belong on the client dossier).
  const { countForCompanyAggregated } = await import('@/lib/services/notes.service');
  const noteCount = await countForCompanyAggregated(portId, id).catch(() => 0);

  return {
    ...rest,
    tags: tagJoins.map((t) => t.tag),
    addresses,
    noteCount,
  };
}

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

export async function updateCompany(
  id: string,
  portId: string,
  data: UpdateCompanyInput,
  meta: AuditMeta,
) {
  const existing = await db.query.companies.findFirst({
    where: eq(companies.id, id),
  });

  if (!existing || existing.portId !== portId) {
    throw new NotFoundError('Company');
  }

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

  let updated: Company | undefined;
  try {
    const rows = await db
      .update(companies)
      .set({ ...data, updatedAt: new Date() })
      .where(and(eq(companies.id, id), eq(companies.portId, portId)))
      .returning();
    updated = rows[0];
  } catch (err) {
    if (isUniqueViolation(err)) {
      throw new ConflictError('company name already exists');
    }
    throw err;
  }

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

  emitToRoom(`port:${portId}`, 'company:updated', {
    companyId: id,
    changedFields: Object.keys(diff),
  });

  if (data.name !== undefined) {
    await syncEntityFolderName(portId, 'company', id, meta.userId).catch((err) => {
      logger.warn({ err, companyId: id, portId }, 'Failed to sync company folder name');
    });
  }

  return updated!;
}

// ─── Archive ─────────────────────────────────────────────────────────────────

export async function archiveCompany(id: string, portId: string, meta: AuditMeta) {
  const existing = await db.query.companies.findFirst({
    where: eq(companies.id, id),
  });

  if (!existing || existing.portId !== portId) {
    throw new NotFoundError('Company');
  }

  // NOTE: bypassing the shared `softDelete(...)` util: it sets the raw column key
  // `archived_at`, which Drizzle does not recognise (the JS key is `archivedAt`)
  // and therefore emits an empty SET clause. Until the utility is fixed, do the
  // update inline. (See Task 2.3 for context.)
  await db
    .update(companies)
    .set({ archivedAt: new Date() })
    .where(and(eq(companies.id, id), eq(companies.portId, portId)));

  void applyEntityArchivedSuffix(portId, 'company', id, meta.userId).catch((err) => {
    logger.warn(
      { err, companyId: id, portId },
      'Failed to apply archived suffix to company folder',
    );
  });

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'archive',
    entityType: 'company',
    entityId: id,
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  emitToRoom(`port:${portId}`, 'company:archived', { companyId: id });
}

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

export async function listCompanies(portId: string, query: ListCompaniesInput) {
  const { page, limit, sort, order, search, includeArchived, status } = query;

  const filters = [];
  if (status) filters.push(eq(companies.status, status));

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

  const result = await buildListQuery<Company>({
    table: companies,
    portIdColumn: companies.portId,
    portId,
    idColumn: companies.id,
    updatedAtColumn: companies.updatedAt,
    searchColumns: [companies.name, companies.legalName, companies.taxId],
    searchTerm: search,
    filters,
    sort: sort ? { column: sortColumn, direction: order } : undefined,
    page,
    pageSize: limit,
    includeArchived,
    archivedAtColumn: companies.archivedAt,
  });

  if (result.data.length === 0) return result;

  const ids = result.data.map((r) => r.id);

  const [memberCounts, yachtCounts] = await Promise.all([
    db
      .select({ companyId: companyMemberships.companyId, count: count() })
      .from(companyMemberships)
      .where(and(inArray(companyMemberships.companyId, ids), isNull(companyMemberships.endDate)))
      .groupBy(companyMemberships.companyId),
    db
      .select({ ownerId: yachts.currentOwnerId, count: count() })
      .from(yachts)
      .where(
        and(
          eq(yachts.portId, portId),
          eq(yachts.currentOwnerType, 'company'),
          inArray(yachts.currentOwnerId, ids),
          isNull(yachts.archivedAt),
        ),
      )
      .groupBy(yachts.currentOwnerId),
  ]);

  const memberCountMap = new Map(memberCounts.map((r) => [r.companyId, r.count]));
  const yachtCountMap = new Map(yachtCounts.map((r) => [r.ownerId, r.count]));

  return {
    ...result,
    data: result.data.map((row) => ({
      ...row,
      memberCount: memberCountMap.get(row.id) ?? 0,
      yachtCount: yachtCountMap.get(row.id) ?? 0,
    })),
  };
}

// ─── Autocomplete ────────────────────────────────────────────────────────────

export async function autocomplete(portId: string, q: string) {
  // Empty query → return the 10 most-recently-updated companies for the
  // port so the picker has something to scan on first open. Non-empty
  // query → ilike-match against name + legalName as before.
  const trimmed = q.trim();
  const baseQuery = db.select().from(companies);
  if (!trimmed) {
    return await baseQuery
      .where(eq(companies.portId, portId))
      .orderBy(desc(companies.updatedAt))
      .limit(10);
  }
  const pattern = `%${trimmed}%`;
  return await baseQuery
    .where(
      and(
        eq(companies.portId, portId),
        or(ilike(companies.name, pattern), ilike(companies.legalName, pattern)),
      ),
    )
    .limit(10);
}

// ─── Upsert by name (find-or-create) ─────────────────────────────────────────

/**
 * Find-or-create a company by (portId, lower(name)). NOT a Postgres UPSERT.
 *
 * Runs a case-insensitive SELECT scoped by portId; if found, returns it.
 * Otherwise inserts a new row with the provided `name` verbatim. A concurrent
 * insert that hits the partial unique index (23505) is re-raised as
 * ConflictError for the caller to retry if desired.
 */
export async function upsertByName(portId: string, name: string, meta: AuditMeta) {
  return await withTransaction(async (tx) => {
    const existing = await tx.query.companies.findFirst({
      where: and(eq(companies.portId, portId), sql`lower(${companies.name}) = lower(${name})`),
    });
    if (existing) return existing;

    try {
      const [company] = await tx
        .insert(companies)
        .values({
          portId,
          name,
          status: 'active',
        })
        .returning();

      void createAuditLog({
        userId: meta.userId,
        portId,
        action: 'create',
        entityType: 'company',
        entityId: company!.id,
        newValue: { name: company!.name, status: company!.status },
        ipAddress: meta.ipAddress,
        userAgent: meta.userAgent,
      });

      emitToRoom(`port:${portId}`, 'company:created', { companyId: company!.id });

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

export async function setCompanyTags(
  companyId: string,
  portId: string,
  tagIds: string[],
  meta: AuditMeta,
) {
  const company = await db.query.companies.findFirst({ where: eq(companies.id, companyId) });
  if (!company || company.portId !== portId) throw new NotFoundError('Company');

  await setEntityTags({
    joinTable: companyTags,
    entityColumn: companyTags.companyId,
    tagColumn: companyTags.tagId,
    entityId: companyId,
    portId,
    tagIds,
    meta,
    entityType: 'company',
  });
}

// ─── Addresses ────────────────────────────────────────────────────────────────

interface CompanyAddressInput {
  label?: string;
  streetAddress?: string | null;
  city?: string | null;
  subdivisionIso?: string | null;
  postalCode?: string | null;
  countryIso?: string | null;
  isPrimary?: boolean;
}

export async function listCompanyAddresses(companyId: string, portId: string) {
  const company = await db.query.companies.findFirst({ where: eq(companies.id, companyId) });
  if (!company || company.portId !== portId) throw new NotFoundError('Company');

  return db.query.companyAddresses.findMany({
    where: eq(companyAddresses.companyId, companyId),
    orderBy: (t, { desc }) => [desc(t.isPrimary), desc(t.createdAt)],
  });
}

export async function addCompanyAddress(
  companyId: string,
  portId: string,
  data: CompanyAddressInput,
  meta: AuditMeta,
) {
  const company = await db.query.companies.findFirst({ where: eq(companies.id, companyId) });
  if (!company || company.portId !== portId) throw new NotFoundError('Company');

  const address = await withTransaction(async (tx) => {
    // Lock the company row to serialize concurrent primary-toggle requests.
    await tx
      .select({ id: companies.id })
      .from(companies)
      .where(eq(companies.id, companyId))
      .for('update');

    const wantsPrimary = data.isPrimary ?? false;
    if (wantsPrimary) {
      await tx
        .update(companyAddresses)
        .set({ isPrimary: false })
        .where(
          and(eq(companyAddresses.companyId, companyId), eq(companyAddresses.isPrimary, true)),
        );
    }
    const [row] = await tx
      .insert(companyAddresses)
      .values({
        companyId,
        portId,
        label: data.label ?? 'Primary',
        streetAddress: data.streetAddress ?? null,
        city: data.city ?? null,
        subdivisionIso: data.subdivisionIso ?? null,
        postalCode: data.postalCode ?? null,
        countryIso: data.countryIso ?? null,
        isPrimary: wantsPrimary,
      })
      .returning();
    return row!;
  });

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'create',
    entityType: 'companyAddress',
    entityId: address.id,
    newValue: { companyId, label: address.label, countryIso: address.countryIso },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  emitToRoom(`port:${portId}`, 'company:updated', { companyId, changedFields: ['addresses'] });

  return address;
}

export async function updateCompanyAddress(
  addressId: string,
  companyId: string,
  portId: string,
  data: CompanyAddressInput,
  _meta: AuditMeta,
) {
  const company = await db.query.companies.findFirst({ where: eq(companies.id, companyId) });
  if (!company || company.portId !== portId) throw new NotFoundError('Company');

  const existing = await db.query.companyAddresses.findFirst({
    where: and(eq(companyAddresses.id, addressId), eq(companyAddresses.companyId, companyId)),
  });
  if (!existing) throw new NotFoundError('Address');

  const updated = await withTransaction(async (tx) => {
    // Lock the company row to serialize primary-toggle changes.
    await tx
      .select({ id: companies.id })
      .from(companies)
      .where(eq(companies.id, companyId))
      .for('update');

    if (data.isPrimary === true && !existing.isPrimary) {
      await tx
        .update(companyAddresses)
        .set({ isPrimary: false })
        .where(
          and(eq(companyAddresses.companyId, companyId), eq(companyAddresses.isPrimary, true)),
        );
    }
    const [row] = await tx
      .update(companyAddresses)
      .set({ ...data, updatedAt: new Date() })
      .where(eq(companyAddresses.id, addressId))
      .returning();
    return row!;
  });

  emitToRoom(`port:${portId}`, 'company:updated', { companyId, changedFields: ['addresses'] });

  return updated;
}

export async function removeCompanyAddress(
  addressId: string,
  companyId: string,
  portId: string,
  _meta: AuditMeta,
) {
  const company = await db.query.companies.findFirst({ where: eq(companies.id, companyId) });
  if (!company || company.portId !== portId) throw new NotFoundError('Company');

  const address = await db.query.companyAddresses.findFirst({
    where: and(eq(companyAddresses.id, addressId), eq(companyAddresses.companyId, companyId)),
  });
  if (!address) throw new NotFoundError('Address');

  await db.delete(companyAddresses).where(eq(companyAddresses.id, addressId));

  emitToRoom(`port:${portId}`, 'company:updated', { companyId, changedFields: ['addresses'] });
}
