import pLimit from 'p-limit';
import { and, eq, count } from 'drizzle-orm';

import { db } from '@/lib/db';
import { customFieldDefinitions, customFieldValues } from '@/lib/db/schema/system';
import { createAuditLog, type AuditMeta } from '@/lib/audit';
import { CodedError, NotFoundError, ValidationError, ConflictError } from '@/lib/errors';
import type { CreateFieldInput, UpdateFieldInput } from '@/lib/validators/custom-fields';
import type { CustomFieldDefinition } from '@/lib/db/schema/system';

// ─── Types ────────────────────────────────────────────────────────────────────

// ─── Value Validation ─────────────────────────────────────────────────────────

function validateCustomFieldValue(
  definition: CustomFieldDefinition,
  value: unknown,
): string | null {
  if (value === null || value === undefined) {
    return definition.isRequired ? 'This field is required' : null;
  }
  switch (definition.fieldType) {
    case 'text':
      return typeof value !== 'string'
        ? 'Must be text'
        : value.length > 1000
          ? 'Max 1000 chars'
          : null;
    case 'number':
      return typeof value !== 'number' || isNaN(value) ? 'Must be a number' : null;
    case 'date':
      return typeof value !== 'string' || isNaN(Date.parse(value)) ? 'Must be a valid date' : null;
    case 'boolean':
      return typeof value !== 'boolean' ? 'Must be true or false' : null;
    case 'select': {
      const options = (definition.selectOptions as string[] | null) ?? [];
      return !options.includes(value as string) ? `Must be one of: ${options.join(', ')}` : null;
    }
    default:
      return 'Unknown field type';
  }
}

// ─── Definitions ──────────────────────────────────────────────────────────────

export async function listDefinitions(portId: string, entityType?: string) {
  const conditions = [eq(customFieldDefinitions.portId, portId)];
  if (entityType) {
    conditions.push(eq(customFieldDefinitions.entityType, entityType));
  }

  return db.query.customFieldDefinitions.findMany({
    where: and(...conditions),
    orderBy: (fields, { asc }) => [asc(fields.sortOrder), asc(fields.createdAt)],
  });
}

export async function createDefinition(
  portId: string,
  userId: string,
  data: CreateFieldInput,
  meta: AuditMeta,
) {
  // Check for duplicate fieldName within portId + entityType
  const existing = await db.query.customFieldDefinitions.findFirst({
    where: and(
      eq(customFieldDefinitions.portId, portId),
      eq(customFieldDefinitions.entityType, data.entityType),
      eq(customFieldDefinitions.fieldName, data.fieldName),
    ),
  });
  if (existing) {
    throw new ConflictError(
      `A custom field named "${data.fieldName}" already exists for ${data.entityType}`,
    );
  }

  const rows = await db
    .insert(customFieldDefinitions)
    .values({
      portId,
      entityType: data.entityType,
      fieldName: data.fieldName,
      fieldLabel: data.fieldLabel,
      fieldType: data.fieldType,
      selectOptions: data.selectOptions ?? null,
      isRequired: data.isRequired,
      sortOrder: data.sortOrder,
    })
    .returning();

  const created = rows[0];
  if (!created)
    throw new CodedError('INSERT_RETURNING_EMPTY', {
      internalMessage: 'Custom field definition insert returned no row',
    });

  void createAuditLog({
    userId,
    portId,
    action: 'create',
    entityType: 'custom_field_definition',
    entityId: created.id,
    newValue: {
      fieldName: created.fieldName,
      fieldLabel: created.fieldLabel,
      fieldType: created.fieldType,
      entityType: created.entityType,
    },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  return created;
}

export async function updateDefinition(
  portId: string,
  fieldId: string,
  userId: string,
  data: UpdateFieldInput & { fieldType?: unknown },
  meta: AuditMeta,
) {
  // Immutability guard - fieldType must never change
  if ('fieldType' in data && data.fieldType !== undefined) {
    throw new ValidationError('Field type cannot be changed after creation');
  }

  const existing = await db.query.customFieldDefinitions.findFirst({
    where: and(eq(customFieldDefinitions.id, fieldId), eq(customFieldDefinitions.portId, portId)),
  });
  if (!existing) {
    throw new NotFoundError('Custom field definition');
  }

  const updateRows = await db
    .update(customFieldDefinitions)
    .set({
      ...(data.fieldLabel !== undefined && { fieldLabel: data.fieldLabel }),
      ...(data.selectOptions !== undefined && { selectOptions: data.selectOptions }),
      ...(data.isRequired !== undefined && { isRequired: data.isRequired }),
      ...(data.sortOrder !== undefined && { sortOrder: data.sortOrder }),
    })
    // M-MT01: defense-in-depth port_id filter on the UPDATE WHERE.
    // The findFirst above is the primary tenant check, but a concurrent
    // port-swap (or a future cache-jitter path that lets the existing
    // pointer outlive its read) would otherwise let the write land
    // against a sibling port's row with the same id. The entry-point
    // and the write share the same tuple identity now.
    .where(and(eq(customFieldDefinitions.id, fieldId), eq(customFieldDefinitions.portId, portId)))
    .returning();

  const updated = updateRows[0];
  if (!updated)
    throw new CodedError('INSERT_RETURNING_EMPTY', {
      internalMessage: 'Custom field definition update returned no row',
    });

  void createAuditLog({
    userId,
    portId,
    action: 'update',
    entityType: 'custom_field_definition',
    entityId: fieldId,
    oldValue: {
      fieldLabel: existing.fieldLabel,
      selectOptions: existing.selectOptions,
      isRequired: existing.isRequired,
      sortOrder: existing.sortOrder,
    },
    newValue: {
      fieldLabel: updated.fieldLabel,
      selectOptions: updated.selectOptions,
      isRequired: updated.isRequired,
      sortOrder: updated.sortOrder,
    },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  return updated;
}

export async function deleteDefinition(
  portId: string,
  fieldId: string,
  userId: string,
  meta: AuditMeta,
) {
  const existing = await db.query.customFieldDefinitions.findFirst({
    where: and(eq(customFieldDefinitions.id, fieldId), eq(customFieldDefinitions.portId, portId)),
  });
  if (!existing) {
    throw new NotFoundError('Custom field definition');
  }

  // Count associated values before deletion
  const countResult = await db
    .select({ count: count() })
    .from(customFieldValues)
    .where(eq(customFieldValues.fieldId, fieldId));
  const valueCount = countResult[0]?.count ?? 0;

  // Delete definition - CASCADE handles values
  await db.delete(customFieldDefinitions).where(eq(customFieldDefinitions.id, fieldId));

  void createAuditLog({
    userId,
    portId,
    action: 'delete',
    entityType: 'custom_field_definition',
    entityId: fieldId,
    oldValue: {
      fieldName: existing.fieldName,
      fieldLabel: existing.fieldLabel,
      fieldType: existing.fieldType,
      entityType: existing.entityType,
    },
    metadata: { deletedValueCount: Number(valueCount) },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  return { deletedValueCount: Number(valueCount) };
}

// ─── Values ───────────────────────────────────────────────────────────────────

export async function getValues(entityId: string, portId: string) {
  const definitions = await db.query.customFieldDefinitions.findMany({
    where: eq(customFieldDefinitions.portId, portId),
    orderBy: (fields, { asc }) => [asc(fields.sortOrder), asc(fields.createdAt)],
  });

  const values = await db.query.customFieldValues.findMany({
    where: eq(customFieldValues.entityId, entityId),
  });

  const valueMap = new Map(values.map((v) => [v.fieldId, v]));

  return definitions.map((definition) => ({
    definition,
    value: valueMap.get(definition.id) ?? null,
  }));
}

export async function setValues(
  entityId: string,
  portId: string,
  userId: string,
  values: Array<{ fieldId: string; value: unknown }>,
  meta: AuditMeta,
) {
  if (values.length === 0) return [];

  // Fetch relevant definitions to validate values
  const fieldIds = values.map((v) => v.fieldId);
  const definitions = await db.query.customFieldDefinitions.findMany({
    where: eq(customFieldDefinitions.portId, portId),
  });

  const definitionMap = new Map(definitions.map((d) => [d.id, d]));

  // Validate each value
  const errors: Array<{ field: string; message: string }> = [];
  for (const { fieldId, value } of values) {
    const definition = definitionMap.get(fieldId);
    if (!definition) {
      errors.push({ field: fieldId, message: 'Custom field not found for this port' });
      continue;
    }
    const error = validateCustomFieldValue(definition, value);
    if (error) {
      errors.push({ field: definition.fieldName, message: error });
    }
  }

  if (errors.length > 0) {
    throw new ValidationError('Custom field validation failed', errors);
  }

  // Tenant scope: verify entityId actually points at a port-scoped row of
  // the entity type the field definitions target. Without this gate, any
  // authenticated user could write custom-field rows pointing at arbitrary
  // entityIds (or none at all) - polluting customFieldValues and creating
  // a join surface that could later leak data.
  const entityTypes = new Set(
    values
      .map((v) => definitionMap.get(v.fieldId)?.entityType)
      .filter((t): t is string => Boolean(t)),
  );
  for (const entityType of entityTypes) {
    const { eq: drizzleEq, and: drizzleAnd } = await import('drizzle-orm');
    let exists = false;
    if (entityType === 'client') {
      const { clients } = await import('@/lib/db/schema/clients');
      const row = await db.query.clients.findFirst({
        where: drizzleAnd(drizzleEq(clients.id, entityId), drizzleEq(clients.portId, portId)),
      });
      exists = Boolean(row);
    } else if (entityType === 'interest') {
      const { interests } = await import('@/lib/db/schema/interests');
      const row = await db.query.interests.findFirst({
        where: drizzleAnd(drizzleEq(interests.id, entityId), drizzleEq(interests.portId, portId)),
      });
      exists = Boolean(row);
    } else if (entityType === 'berth') {
      const { berths } = await import('@/lib/db/schema/berths');
      const row = await db.query.berths.findFirst({
        where: drizzleAnd(drizzleEq(berths.id, entityId), drizzleEq(berths.portId, portId)),
      });
      exists = Boolean(row);
    } else if (entityType === 'yacht') {
      const { yachts } = await import('@/lib/db/schema/yachts');
      const row = await db.query.yachts.findFirst({
        where: drizzleAnd(drizzleEq(yachts.id, entityId), drizzleEq(yachts.portId, portId)),
      });
      exists = Boolean(row);
    } else if (entityType === 'company') {
      const { companies } = await import('@/lib/db/schema/companies');
      const row = await db.query.companies.findFirst({
        where: drizzleAnd(drizzleEq(companies.id, entityId), drizzleEq(companies.portId, portId)),
      });
      exists = Boolean(row);
    } else {
      throw new ValidationError(`Unsupported custom-field entity type: ${entityType}`);
    }
    if (!exists) {
      throw new ValidationError(`${entityType} not found in this port`);
    }
  }

  // Upsert all values with bounded concurrency. Custom-field sets are
  // typically 5-15 fields, but a port admin could stack 50+ definitions
  // on a single client. Cap at 8 in flight so the pg pool isn't swamped
  // on a bulk-update fan-out.
  const upsertLimit = pLimit(8);
  const results = await Promise.all(
    values.map(({ fieldId, value }) =>
      upsertLimit(async () => {
        const [upserted] = await db
          .insert(customFieldValues)
          .values({
            fieldId,
            entityId,
            value: value as Record<string, unknown>,
            updatedAt: new Date(),
          })
          .onConflictDoUpdate({
            target: [customFieldValues.fieldId, customFieldValues.entityId],
            set: {
              value: value as Record<string, unknown>,
              updatedAt: new Date(),
            },
          })
          .returning();
        return upserted;
      }),
    ),
  );

  void createAuditLog({
    userId,
    portId,
    action: 'update',
    entityType: 'custom_field_values',
    entityId,
    metadata: { fieldIds, updatedCount: results.length },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  return results;
}
