/**
 * Wipe-and-rewrite tag-assignment helper, shared by every entity that has
 * a many-to-many tag join table (`client_tags`, `yacht_tags`, …).
 *
 * The recipe is the same for every entity:
 *   1. wipe the join rows for this entity
 *   2. insert the new set
 *   3. write an audit log
 *   4. emit a socket event
 *
 * This helper performs the wipe+insert inside a single transaction so a
 * partial failure can't leave the entity with zero tags, and standardizes
 * the audit-log payload to `newValue: { tagIds }` across all entity types.
 */

import { and, eq, inArray, type Column } from 'drizzle-orm';
import type { PgTable } from 'drizzle-orm/pg-core';

import { db } from '@/lib/db';
import { tags } from '@/lib/db/schema/system';
import { withTransaction } from '@/lib/db/utils';
import { createAuditLog, type AuditMeta } from '@/lib/audit';
import { ValidationError } from '@/lib/errors';
import { emitToRoom } from '@/lib/socket/server';

interface SetEntityTagsArgs<TJoin extends PgTable> {
  /** Join table - e.g. `clientTags`, `yachtTags`. */
  joinTable: TJoin;
  /**
   * Column on the join table that points back at the parent entity (e.g.
   * `clientTags.clientId`). Used both for the WHERE on delete and as the
   * insert payload key.
   */
  entityColumn: Column;
  /** Column on the join table that holds the tag ID (e.g. `clientTags.tagId`). */
  tagColumn: Column;
  /** Owning entity ID. */
  entityId: string;
  /** Tenant scope. */
  portId: string;
  /** Final desired set of tag IDs (replaces current set). */
  tagIds: string[];
  /** Audit log + socket fields. */
  meta: AuditMeta;
  /**
   * Audit/socket entity discriminator. Drives both `entityType` on the
   * audit row and the socket event name (`<entityType>:updated`).
   */
  entityType: 'client' | 'company' | 'yacht' | 'interest' | 'berth';
}

export async function setEntityTags<TJoin extends PgTable>(
  args: SetEntityTagsArgs<TJoin>,
): Promise<{ entityId: string; tagIds: string[] }> {
  const { joinTable, entityColumn, tagColumn, entityId, portId, tagIds, meta, entityType } = args;

  // Tenant scope: every supplied tagId must belong to the caller's port.
  // The tags table is per-port (`tags.port_id`) but the join tables only
  // have a single-column FK to `tags.id` - without this guard, a port-A
  // caller could splice a port-B tag UUID onto their own entity. The
  // entity's GET handler joins `tags ON join.tag_id = tags.id` with no
  // port filter, so the foreign tag's name and color render in port A.
  if (tagIds.length > 0) {
    const uniqueTagIds = [...new Set(tagIds)];
    const validTags = await db
      .select({ id: tags.id })
      .from(tags)
      .where(and(inArray(tags.id, uniqueTagIds), eq(tags.portId, portId)));
    if (validTags.length !== uniqueTagIds.length) {
      throw new ValidationError('One or more tags are not in this port');
    }
  }

  await withTransaction(async (tx) => {
    await tx.delete(joinTable).where(eq(entityColumn, entityId));
    if (tagIds.length > 0) {
      await tx.insert(joinTable).values(
        tagIds.map(
          (tagId) =>
            ({
              [entityColumn.name]: entityId,
              [tagColumn.name]: tagId,
            }) as TJoin['$inferInsert'],
        ),
      );
    }
  });

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'update',
    entityType,
    entityId,
    newValue: { tagIds },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  // Per-event payloads use entity-specific keys (e.g. `clientId` vs `yachtId`),
  // so dispatch through a switch rather than a computed property to keep the
  // ServerToClientEvents typing happy.
  const room = `port:${portId}` as const;
  const changedFields = ['tags'] as const;
  switch (entityType) {
    case 'client':
      emitToRoom(room, 'client:updated', { clientId: entityId, changedFields: [...changedFields] });
      break;
    case 'company':
      emitToRoom(room, 'company:updated', {
        companyId: entityId,
        changedFields: [...changedFields],
      });
      break;
    case 'yacht':
      emitToRoom(room, 'yacht:updated', { yachtId: entityId, changedFields: [...changedFields] });
      break;
    case 'interest':
      emitToRoom(room, 'interest:updated', {
        interestId: entityId,
        changedFields: [...changedFields],
      });
      break;
    case 'berth':
      emitToRoom(room, 'berth:updated', { berthId: entityId, changedFields: [...changedFields] });
      break;
  }

  return { entityId, tagIds };
}
