/**
 * Admin Document Template Service - TipTap JSON-based templates
 *
 * This service manages templates whose content is stored as TipTap JSON
 * (serialised to the `bodyHtml` text column). Version history is maintained
 * via audit_log entries (action='update', entityType='document_template',
 * metadata.version + metadata.content).
 *
 * Template type values: eoi | contract | nda | reservation_agreement | letter | other
 * These are stored in the `templateType` column.
 */

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

import { db } from '@/lib/db';
import { documentTemplates } from '@/lib/db/schema/documents';
import { auditLogs } from '@/lib/db/schema/system';
import { createAuditLog, type AuditMeta } from '@/lib/audit';
import { NotFoundError, ValidationError } from '@/lib/errors';
import { validateTipTapDocument } from '@/lib/pdf/tiptap-validation';
import type {
  CreateAdminTemplateInput,
  UpdateAdminTemplateInput,
  ListAdminTemplatesInput,
} from '@/lib/validators/document-templates';

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

/**
 * A version entry reconstructed from audit_log records.
 */
export interface TemplateVersion {
  version: number;
  content: Record<string, unknown>;
  changedBy: string | null;
  changedAt: Date;
  auditLogId: string;
}

/**
 * Helper: extract the numeric version stored in a templateType-encoded field.
 * We use a convention: version is stored in the `mergeFields` jsonb array
 * as `["__version__:N"]` to avoid adding a new column.
 */
function getVersionFromRecord(record: typeof documentTemplates.$inferSelect): number {
  const mf = record.mergeFields as unknown;
  if (!Array.isArray(mf)) return 1;
  const versionEntry = (mf as string[]).find((e) => e.startsWith('__version__:'));
  if (!versionEntry) return 1;
  const n = parseInt(versionEntry.split(':')[1] ?? '1', 10);
  return isNaN(n) ? 1 : n;
}

function buildMergeFieldsWithVersion(version: number): string[] {
  return [`__version__:${version}`];
}

/**
 * Parse TipTap JSON from bodyHtml field. Returns the parsed object, or null
 * if bodyHtml is plain HTML (legacy records).
 */
function parseTipTapContent(bodyHtml: string): Record<string, unknown> | null {
  try {
    const parsed = JSON.parse(bodyHtml) as unknown;
    if (
      parsed !== null &&
      typeof parsed === 'object' &&
      'type' in (parsed as Record<string, unknown>)
    ) {
      return parsed as Record<string, unknown>;
    }
    return null;
  } catch {
    return null;
  }
}

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

export async function listAdminTemplates(portId: string, query: ListAdminTemplatesInput) {
  const { type, isActive } = query;

  const conditions = [eq(documentTemplates.portId, portId)];

  if (type) {
    conditions.push(eq(documentTemplates.templateType, type));
  }
  if (isActive !== undefined) {
    conditions.push(eq(documentTemplates.isActive, isActive));
  }

  const rows = await db
    .select()
    .from(documentTemplates)
    .where(and(...conditions))
    .orderBy(documentTemplates.name);

  return rows.map((row) => ({
    ...row,
    version: getVersionFromRecord(row),
    content: parseTipTapContent(row.bodyHtml ?? ''),
  }));
}

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

export async function getAdminTemplate(portId: string, templateId: string) {
  const row = await db.query.documentTemplates.findFirst({
    where: and(eq(documentTemplates.id, templateId), eq(documentTemplates.portId, portId)),
  });

  if (!row) {
    throw new NotFoundError('Document template');
  }

  return {
    ...row,
    version: getVersionFromRecord(row),
    content: parseTipTapContent(row.bodyHtml ?? ''),
  };
}

// ─── Validate TipTap Content ─────────────────────────────────────────────────

function assertValidContent(content: Record<string, unknown>): void {
  const unsupported = validateTipTapDocument(
    content as unknown as Parameters<typeof validateTipTapDocument>[0],
  );
  if (unsupported.length > 0) {
    throw new ValidationError(
      `Template content contains unsupported node types: ${unsupported.join(', ')}. ` +
        'Supported: paragraph, heading (h1-h3), bulletList, orderedList, listItem, ' +
        'table, tableRow, tableCell, tableHeader, image, hardBreak, text.',
    );
  }
}

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

export async function createAdminTemplate(
  portId: string,
  userId: string,
  data: CreateAdminTemplateInput,
  meta: AuditMeta,
) {
  assertValidContent(data.content);

  const [template] = await db
    .insert(documentTemplates)
    .values({
      portId,
      name: data.name,
      templateType: data.type,
      bodyHtml: JSON.stringify(data.content),
      mergeFields: buildMergeFieldsWithVersion(1),
      isActive: true,
      createdBy: userId,
    })
    .returning();

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'create',
    entityType: 'document_template',
    entityId: template!.id,
    newValue: { name: template!.name, type: data.type, version: 1 },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  return {
    ...template!,
    version: 1,
    content: data.content,
  };
}

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

export async function updateAdminTemplate(
  portId: string,
  templateId: string,
  userId: string,
  data: UpdateAdminTemplateInput,
  meta: AuditMeta,
) {
  const existing = await getAdminTemplate(portId, templateId);

  if (data.content !== undefined) {
    assertValidContent(data.content);
  }

  const currentVersion = existing.version;
  const newVersion = data.content !== undefined ? currentVersion + 1 : currentVersion;

  // Before updating content, save old content to audit log for versioning
  if (data.content !== undefined) {
    void createAuditLog({
      userId: meta.userId,
      portId,
      action: 'update',
      entityType: 'document_template',
      entityId: templateId,
      oldValue: { version: currentVersion, name: existing.name },
      newValue: { version: newVersion, name: data.name ?? existing.name },
      metadata: {
        versionSnapshot: currentVersion,
        content: existing.content ?? {},
      },
      ipAddress: meta.ipAddress,
      userAgent: meta.userAgent,
    });
  }

  const updateValues: Partial<typeof documentTemplates.$inferInsert> = {
    updatedAt: new Date(),
  };

  if (data.name !== undefined) {
    updateValues.name = data.name;
  }
  if (data.content !== undefined) {
    updateValues.bodyHtml = JSON.stringify(data.content);
    updateValues.mergeFields = buildMergeFieldsWithVersion(newVersion);
  }
  if (data.isActive !== undefined) {
    updateValues.isActive = data.isActive;
  }

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

  return {
    ...updated!,
    version: newVersion,
    content: data.content !== undefined ? data.content : existing.content,
  };
}

// ─── Delete ───────────────────────────────────────────────────────────────────

export async function deleteAdminTemplate(
  portId: string,
  templateId: string,
  userId: string,
  meta: AuditMeta,
) {
  const existing = await getAdminTemplate(portId, templateId);

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

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'delete',
    entityType: 'document_template',
    entityId: templateId,
    oldValue: { name: existing.name, type: existing.templateType, version: existing.version },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });
}

// ─── Version History ──────────────────────────────────────────────────────────

/**
 * Retrieves version history for a template by querying audit_logs.
 * Each 'update' audit log entry with entityType='document_template' and
 * metadata.versionSnapshot contains a saved version.
 */
export async function getAdminTemplateVersions(
  portId: string,
  templateId: string,
): Promise<TemplateVersion[]> {
  // Verify template exists and belongs to port
  await getAdminTemplate(portId, templateId);

  const logs = await db
    .select()
    .from(auditLogs)
    .where(
      and(
        eq(auditLogs.entityType, 'document_template'),
        eq(auditLogs.entityId, templateId),
        eq(auditLogs.action, 'update'),
        eq(auditLogs.portId, portId),
      ),
    )
    .orderBy(desc(auditLogs.createdAt));

  return logs
    .filter((log) => {
      const meta = log.metadata as Record<string, unknown> | null;
      return (
        meta !== null && typeof meta === 'object' && 'versionSnapshot' in meta && 'content' in meta
      );
    })
    .map((log) => {
      const meta = log.metadata as Record<string, unknown>;
      return {
        version: meta.versionSnapshot as number,
        content: meta.content as Record<string, unknown>,
        changedBy: log.userId,
        changedAt: log.createdAt,
        auditLogId: log.id,
      };
    });
}

// ─── Rollback ─────────────────────────────────────────────────────────────────

/**
 * Restores a template to a previous version found in audit_logs.
 * Creates a new version number (current + 1) with the restored content.
 */
export async function rollbackAdminTemplate(
  portId: string,
  templateId: string,
  version: number,
  userId: string,
  meta: AuditMeta,
) {
  const existing = await getAdminTemplate(portId, templateId);
  const versions = await getAdminTemplateVersions(portId, templateId);

  const targetVersion = versions.find((v) => v.version === version);
  if (!targetVersion) {
    throw new NotFoundError(`Template version ${version}`);
  }

  const newVersion = existing.version + 1;

  // Save current state to audit log before rollback
  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'update',
    entityType: 'document_template',
    entityId: templateId,
    oldValue: { version: existing.version, name: existing.name },
    newValue: { version: newVersion, name: existing.name, rolledBackTo: version },
    metadata: {
      versionSnapshot: existing.version,
      content: existing.content ?? {},
      rolledBackTo: version,
    },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  const [updated] = await db
    .update(documentTemplates)
    .set({
      bodyHtml: JSON.stringify(targetVersion.content),
      mergeFields: buildMergeFieldsWithVersion(newVersion),
      updatedAt: new Date(),
    })
    .where(and(eq(documentTemplates.id, templateId), eq(documentTemplates.portId, portId)))
    .returning();

  return {
    ...updated!,
    version: newVersion,
    content: targetVersion.content,
    rolledBackFrom: existing.version,
    rolledBackTo: version,
  };
}
