import { and, arrayContains, eq, or } from 'drizzle-orm';

import { db } from '@/lib/db';
import { files, documents } from '@/lib/db/schema/documents';
import { expenses } from '@/lib/db/schema/financial';
import { berthMaintenanceLog } from '@/lib/db/schema/berths';
import { createAuditLog } from '@/lib/audit';
import { ConflictError, NotFoundError, ValidationError } from '@/lib/errors';
import { emitToRoom } from '@/lib/socket/server';
import { minioClient, getPresignedUrl } from '@/lib/minio';
import { buildListQuery } from '@/lib/db/query-builder';
import { env } from '@/lib/env';
import {
  ALLOWED_MIME_TYPES,
  MAX_FILE_SIZE,
  PREVIEWABLE_MIMES,
} from '@/lib/constants/file-validation';
import { generateStorageKey, sanitizeFilename } from '@/lib/services/storage';
import type { UploadFileInput, UpdateFileInput, ListFilesInput } from '@/lib/validators/files';

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

interface AuditMeta {
  userId: string;
  portId: string;
  ipAddress: string;
  userAgent: string;
}

interface UploadFileParams {
  buffer: Buffer;
  originalName: string;
  mimeType: string;
  size: number;
}

// ─── Upload ───────────────────────────────────────────────────────────────────

export async function uploadFile(
  portId: string,
  portSlug: string,
  file: UploadFileParams,
  data: UploadFileInput,
  meta: AuditMeta,
) {
  if (!ALLOWED_MIME_TYPES.has(file.mimeType)) {
    throw new ValidationError(`File type '${file.mimeType}' is not allowed`);
  }

  if (file.size > MAX_FILE_SIZE) {
    throw new ValidationError('File exceeds maximum size of 50MB');
  }

  const entity = data.entityType ?? 'general';
  const entityId = data.entityId ?? portId;
  const storagePath = generateStorageKey(portSlug, entity, entityId, file.mimeType);
  const sanitizedOriginal = sanitizeFilename(file.originalName);
  const sanitizedFilename = sanitizeFilename(data.filename);

  await minioClient.putObject(
    env.MINIO_BUCKET,
    storagePath,
    file.buffer,
    file.size,
    { 'Content-Type': file.mimeType },
  );

  const [record] = await db
    .insert(files)
    .values({
      portId,
      clientId: data.clientId ?? null,
      filename: sanitizedFilename,
      originalName: sanitizedOriginal,
      mimeType: file.mimeType,
      sizeBytes: String(file.size),
      storagePath,
      storageBucket: env.MINIO_BUCKET,
      category: data.category ?? null,
      uploadedBy: meta.userId,
    })
    .returning();

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'create',
    entityType: 'file',
    entityId: record!.id,
    newValue: { filename: record!.filename, mimeType: file.mimeType, size: file.size },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  emitToRoom(`port:${portId}`, 'file:uploaded', {
    fileId: record!.id,
    filename: record!.filename,
  });

  return record!;
}

// ─── Download / Preview URLs ──────────────────────────────────────────────────

export async function getDownloadUrl(id: string, portId: string) {
  const file = await getFileById(id, portId);
  const url = await getPresignedUrl(file.storagePath);
  return { url, filename: file.filename };
}

export async function getPreviewUrl(id: string, portId: string) {
  const file = await getFileById(id, portId);

  if (!file.mimeType || !PREVIEWABLE_MIMES.has(file.mimeType)) {
    throw new ValidationError('This file type cannot be previewed');
  }

  const url = await getPresignedUrl(file.storagePath);
  return { url, mimeType: file.mimeType };
}

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

export async function updateFile(
  id: string,
  portId: string,
  data: UpdateFileInput,
  meta: AuditMeta,
) {
  const existing = await getFileById(id, portId);

  const updates: { filename?: string; category?: string } = {};
  if (data.filename !== undefined) updates.filename = sanitizeFilename(data.filename);
  if (data.category !== undefined) updates.category = data.category;

  const [updated] = await db
    .update(files)
    .set(updates)
    .where(and(eq(files.id, id), eq(files.portId, portId)))
    .returning();

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'update',
    entityType: 'file',
    entityId: id,
    oldValue: { filename: existing.filename, category: existing.category },
    newValue: updates,
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  emitToRoom(`port:${portId}`, 'file:updated', { fileId: id });

  return updated!;
}

// ─── Delete (BR-091) ──────────────────────────────────────────────────────────

export async function deleteFile(id: string, portId: string, meta: AuditMeta) {
  const existing = await getFileById(id, portId);

  // BR-091: check references before deleting
  const [docRefs, expenseRefs, maintenanceRefs] = await Promise.all([
    db
      .select({ id: documents.id })
      .from(documents)
      .where(
        and(
          eq(documents.portId, portId),
          or(eq(documents.fileId, id), eq(documents.signedFileId, id)),
        ),
      )
      .limit(1),
    db
      .select({ id: expenses.id })
      .from(expenses)
      .where(
        and(
          eq(expenses.portId, portId),
          arrayContains(expenses.receiptFileIds, [id]),
        ),
      )
      .limit(1),
    db
      .select({ id: berthMaintenanceLog.id })
      .from(berthMaintenanceLog)
      .where(
        and(
          eq(berthMaintenanceLog.portId, portId),
          arrayContains(berthMaintenanceLog.photoFileIds, [id]),
        ),
      )
      .limit(1),
  ]);

  if (docRefs.length > 0 || expenseRefs.length > 0 || maintenanceRefs.length > 0) {
    throw new ConflictError(
      'File cannot be deleted because it is referenced by other records',
    );
  }

  // Delete from MinIO first, then DB
  await minioClient.removeObject(env.MINIO_BUCKET, existing.storagePath);

  await db.delete(files).where(and(eq(files.id, id), eq(files.portId, portId)));

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'delete',
    entityType: 'file',
    entityId: id,
    oldValue: { filename: existing.filename },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  emitToRoom(`port:${portId}`, 'file:deleted', { fileId: id });
}

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

export async function listFiles(portId: string, query: ListFilesInput) {
  const { page, limit, sort, order, search, clientId, category } = query;

  const filters = [];

  if (clientId) {
    filters.push(eq(files.clientId, clientId));
  }
  if (category) {
    filters.push(eq(files.category, category));
  }

  const sortColumn =
    sort === 'filename' ? files.filename :
    sort === 'sizeBytes' ? files.sizeBytes :
    files.createdAt;

  return buildListQuery({
    table: files,
    portIdColumn: files.portId,
    portId,
    idColumn: files.id,
    updatedAtColumn: files.createdAt, // no updatedAt on files
    searchColumns: [files.filename, files.originalName],
    searchTerm: search,
    filters,
    sort: sort ? { column: sortColumn, direction: order } : undefined,
    page,
    pageSize: limit,
    // no archivedAtColumn — files are immutable records
  });
}

// ─── Get by ID ────────────────────────────────────────────────────────────────

export async function getFileById(id: string, portId: string) {
  const file = await db.query.files.findFirst({
    where: eq(files.id, id),
  });

  if (!file || file.portId !== portId) {
    throw new NotFoundError('File');
  }

  return file;
}
