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

import { db } from '@/lib/db';
import { reportTemplates, type ReportTemplate } from '@/lib/db/schema/reports';
import { ConflictError, NotFoundError } from '@/lib/errors';
import { createAuditLog, type AuditMeta } from '@/lib/audit';

/**
 * CRUD for saved PDF-report templates. Multi-tenant safe - every
 * query carries `port_id = ctx.portId` and the unique sibling-name
 * index is `(port_id, kind, LOWER(name))`, so two different ports
 * can both have a "Monthly board report" template without colliding.
 *
 * The `config` field is opaque to this service; the route layer
 * validates it via the same zod discriminated-union schema it uses
 * on `/api/v1/reports/generate`, then passes the parsed value
 * through.
 */

export interface CreateReportTemplateInput {
  portId: string;
  kind: string;
  name: string;
  description?: string | null;
  config: Record<string, unknown>;
  meta: AuditMeta;
}

export async function createReportTemplate(
  input: CreateReportTemplateInput,
): Promise<ReportTemplate> {
  try {
    const [row] = await db
      .insert(reportTemplates)
      .values({
        portId: input.portId,
        kind: input.kind,
        name: input.name,
        description: input.description ?? null,
        config: input.config,
        createdBy: input.meta.userId,
      })
      .returning();
    if (!row) throw new Error('createReportTemplate: insert returned no row');

    void createAuditLog({
      portId: input.portId,
      userId: input.meta.userId,
      action: 'create',
      entityType: 'report_template',
      entityId: row.id,
      newValue: { kind: row.kind, name: row.name },
      ipAddress: input.meta.ipAddress,
      userAgent: input.meta.userAgent,
    });

    return row;
  } catch (err) {
    if (isSiblingNameConflict(err)) {
      throw new ConflictError(
        `A "${input.name}" template already exists for the ${input.kind} kind. Pick a different name.`,
      );
    }
    throw err;
  }
}

export async function listReportTemplates(
  portId: string,
  kind?: string,
): Promise<ReportTemplate[]> {
  const whereClause = kind
    ? and(eq(reportTemplates.portId, portId), eq(reportTemplates.kind, kind))
    : eq(reportTemplates.portId, portId);
  return db
    .select()
    .from(reportTemplates)
    .where(whereClause)
    .orderBy(asc(reportTemplates.kind), asc(reportTemplates.name));
}

export async function getReportTemplate(id: string, portId: string): Promise<ReportTemplate> {
  const row = await db.query.reportTemplates.findFirst({
    where: and(eq(reportTemplates.id, id), eq(reportTemplates.portId, portId)),
  });
  if (!row) throw new NotFoundError('Report template');
  return row;
}

export interface UpdateReportTemplateInput {
  id: string;
  portId: string;
  name?: string;
  description?: string | null;
  config?: Record<string, unknown>;
  meta: AuditMeta;
}

export async function updateReportTemplate(
  input: UpdateReportTemplateInput,
): Promise<ReportTemplate> {
  const existing = await getReportTemplate(input.id, input.portId);
  const patch: Partial<typeof reportTemplates.$inferInsert> = {
    updatedAt: new Date(),
  };
  if (input.name !== undefined) patch.name = input.name;
  if (input.description !== undefined) patch.description = input.description;
  if (input.config !== undefined) patch.config = input.config;

  try {
    const [row] = await db
      .update(reportTemplates)
      .set(patch)
      .where(and(eq(reportTemplates.id, input.id), eq(reportTemplates.portId, input.portId)))
      .returning();
    if (!row) throw new NotFoundError('Report template');

    void createAuditLog({
      portId: input.portId,
      userId: input.meta.userId,
      action: 'update',
      entityType: 'report_template',
      entityId: row.id,
      oldValue: { name: existing.name, description: existing.description },
      newValue: { name: row.name, description: row.description },
      ipAddress: input.meta.ipAddress,
      userAgent: input.meta.userAgent,
    });

    return row;
  } catch (err) {
    if (isSiblingNameConflict(err)) {
      throw new ConflictError(
        `A template with that name already exists for the ${existing.kind} kind.`,
      );
    }
    throw err;
  }
}

export async function deleteReportTemplate(
  id: string,
  portId: string,
  meta: AuditMeta,
): Promise<void> {
  const existing = await getReportTemplate(id, portId);
  await db
    .delete(reportTemplates)
    .where(and(eq(reportTemplates.id, id), eq(reportTemplates.portId, portId)));

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

function isSiblingNameConflict(err: unknown): boolean {
  if (!err || typeof err !== 'object') return false;
  const e = err as {
    code?: unknown;
    constraint_name?: unknown;
    constraint?: unknown;
    cause?: { code?: unknown; constraint_name?: unknown; constraint?: unknown };
  };
  const code = e.code ?? e.cause?.code;
  if (code !== '23505') return false;
  const constraint =
    e.constraint_name ?? e.constraint ?? e.cause?.constraint_name ?? e.cause?.constraint;
  return constraint === 'uniq_report_templates_port_kind_name';
}
