/**
 * AWS Route 53 DNS sync — CRUD write-back persistence.
 *
 * Implements the pending -> committed/failed audit lifecycle, the single
 * most important pattern in this phase (lifted from
 * lib/services/analyzer/asset-audit/persistence.ts's createPendingWrite /
 * markWriteCommitted / markWriteFailed shape, itself following
 * migrations/075_itglue_audit.sql's itglue_writes precedent).
 *
 * Discipline (24-RESEARCH.md Pattern 3, D-07): an audit row is created with
 * status='pending' BEFORE any AWS command is constructed. It is then
 * transitioned to 'committed' or 'failed' after the AWS call resolves.
 * Callers must never construct a ChangeResourceRecordSetsCommand without an
 * audit row already in flight.
 */

import postgresClient from '@/lib/services/postgres-client';
import { sanitizeAwsError } from '@/lib/services/route53-record-validation';
import type { Route53RecordValue } from '@/lib/types/route53';

const LOG_PREFIX = '[ROUTE53-WRITE]';

// ============================================================================
// Audit lifecycle (route53_audit_log)
// ============================================================================

export interface CreatePendingAuditLogInput {
  operation: 'create' | 'update' | 'delete';
  zoneId: string;
  recordKey: string;
  recordName: string;
  recordType: string;
  beforeValue: unknown;
  afterValue: unknown;
  performedByUserId: string | null;
  performedByEmail: string | null;
}

/**
 * Insert a 'pending' audit row. Must be called and awaited BEFORE any AWS
 * ChangeResourceRecordSetsCommand is constructed — that ordering is the
 * whole point of this pattern: no write to Route 53 can occur without an
 * audit row already in flight (D-07, SC-3). This write is not best-effort —
 * let it throw so a DB failure fails the request rather than silently
 * allowing an unlogged DNS mutation (T-24-07).
 */
export async function createPendingAuditLog(
  input: CreatePendingAuditLogInput
): Promise<{ id: string }> {
  const res = await postgresClient.query<{ id: string }>(
    `INSERT INTO route53_audit_log
       (operation, zone_id, record_key, record_name, record_type,
        before_value, after_value,
        performed_by_user_id, performed_by_email, status)
     VALUES ($1, $2, $3, $4, $5,
             $6::jsonb, $7::jsonb,
             $8, $9, 'pending')
     RETURNING id::text AS id`,
    [
      input.operation,
      input.zoneId,
      input.recordKey,
      input.recordName,
      input.recordType,
      JSON.stringify(input.beforeValue ?? null),
      JSON.stringify(input.afterValue ?? null),
      input.performedByUserId,
      input.performedByEmail,
    ]
  );
  return { id: res.rows[0].id };
}

/**
 * Transition an audit row to 'committed' after a successful AWS call. Not
 * best-effort — let it throw.
 */
export async function markAuditCommitted(
  id: string,
  awsChangeId: string | null,
  awsChangeStatus: string | null,
  awsResponse: unknown
): Promise<void> {
  await postgresClient.query(
    `UPDATE route53_audit_log
        SET status = 'committed',
            completed_at = NOW(),
            aws_change_id = $2,
            aws_change_status = $3,
            aws_response = $4::jsonb
      WHERE id = $1`,
    [id, awsChangeId, awsChangeStatus, JSON.stringify(awsResponse ?? null)]
  );
}

/**
 * Transition an audit row to 'failed' after an unsuccessful AWS call.
 * D-07 + T-24-03: the error is always passed through sanitizeAwsError first
 * — never pass a raw error object or JSON.stringify(err) into error_message.
 * Not best-effort — let it throw.
 */
export async function markAuditFailed(id: string, err: unknown): Promise<void> {
  await postgresClient.query(
    `UPDATE route53_audit_log
        SET status = 'failed',
            completed_at = NOW(),
            error_message = $2
      WHERE id = $1`,
    [id, sanitizeAwsError(err)]
  );
}

// ============================================================================
// pulse_crud history (route53_record_history)
// ============================================================================

export interface InsertPulseCrudHistoryInput {
  zoneId: string;
  recordKey: string;
  recordName: string;
  recordType: string;
  changeAction: 'create' | 'update' | 'delete';
  beforeValue: unknown;
  afterValue: unknown;
  changedByUserId: string | null;
  changedByEmail: string | null;
  auditLogId: string;
}

/**
 * Insert a 'pulse_crud' history row.
 *
 * IMPORTANT: callers must invoke this ONLY after markAuditCommitted. A
 * failed AWS call changed nothing on AWS's side, so it gets an audit row
 * (for traceability of the attempt) but must NOT get a history row (which is
 * a timeline of resolved, actually-applied changes). See 24-RESEARCH.md
 * Pattern 3.
 */
export async function insertPulseCrudHistory(
  input: InsertPulseCrudHistoryInput
): Promise<void> {
  await postgresClient.query(
    `INSERT INTO route53_record_history
       (zone_id, record_key, record_name, record_type,
        change_action, before_value, after_value,
        source, changed_by_user_id, changed_by_email, audit_log_id)
     VALUES ($1, $2, $3, $4,
             $5, $6::jsonb, $7::jsonb,
             'pulse_crud', $8, $9, $10)`,
    [
      input.zoneId,
      input.recordKey,
      input.recordName,
      input.recordType,
      input.changeAction,
      JSON.stringify(input.beforeValue ?? null),
      JSON.stringify(input.afterValue ?? null),
      input.changedByUserId,
      input.changedByEmail,
      input.auditLogId,
    ]
  );
}

// ============================================================================
// Mirror refresh (route53_records) — best-effort
// ============================================================================

export interface UpsertMirrorRecordInput {
  recordKey: string;
  zoneId: string;
  name: string;
  type: string;
  setIdentifier: string | null;
  ttl: number | null;
  resourceRecords: Route53RecordValue[] | null;
  aliasTarget: Record<string, unknown> | null;
  rawPayload: unknown;
}

/**
 * Best-effort refresh of route53_records after a committed write so the
 * admin UI reflects the change before the next scheduled sync. A failure
 * here is logged but never thrown — the AWS write already succeeded and the
 * next incremental sync reconciles the mirror regardless.
 */
export async function upsertMirrorRecord(input: UpsertMirrorRecordInput): Promise<void> {
  try {
    await postgresClient.query(
      `INSERT INTO route53_records
         (record_key, zone_id, name, type, set_identifier, ttl,
          resource_records, alias_target, raw_payload,
          synced_at, updated_at, is_deleted, deleted_at)
       VALUES ($1, $2, $3, $4, $5, $6,
               $7::jsonb, $8::jsonb, $9::jsonb,
               NOW(), NOW(), false, NULL)
       ON CONFLICT (record_key) DO UPDATE SET
         zone_id = EXCLUDED.zone_id,
         name = EXCLUDED.name,
         type = EXCLUDED.type,
         set_identifier = EXCLUDED.set_identifier,
         ttl = EXCLUDED.ttl,
         resource_records = EXCLUDED.resource_records,
         alias_target = EXCLUDED.alias_target,
         raw_payload = EXCLUDED.raw_payload,
         synced_at = NOW(),
         updated_at = NOW(),
         is_deleted = false,
         deleted_at = NULL`,
      [
        input.recordKey,
        input.zoneId,
        input.name,
        input.type,
        input.setIdentifier,
        input.ttl,
        JSON.stringify(input.resourceRecords ?? null),
        JSON.stringify(input.aliasTarget ?? null),
        JSON.stringify(input.rawPayload ?? null),
      ]
    );
  } catch (err) {
    console.error(`${LOG_PREFIX} upsertMirrorRecord failed (best-effort):`, sanitizeAwsError(err));
  }
}

/**
 * Soft-delete a mirror record. Never hard-delete: route53_record_history
 * references record_key and the ledger is unbounded by design (D-08). Best
 * effort — logged, never thrown.
 */
export async function softDeleteMirrorRecord(recordKey: string): Promise<void> {
  try {
    await postgresClient.query(
      `UPDATE route53_records
          SET is_deleted = true, deleted_at = NOW(), updated_at = NOW()
        WHERE record_key = $1`,
      [recordKey]
    );
  } catch (err) {
    console.error(`${LOG_PREFIX} softDeleteMirrorRecord failed (best-effort):`, sanitizeAwsError(err));
  }
}

// ============================================================================
// Mirror read (route53_records)
// ============================================================================

export interface MirrorRecordRow {
  recordKey: string;
  zoneId: string;
  name: string;
  type: string;
  setIdentifier: string | null;
  ttl: number | null;
  resourceRecords: Route53RecordValue[] | null;
  aliasTarget: Record<string, unknown> | null;
}

interface RawMirrorRecordRow {
  record_key: string;
  zone_id: string;
  name: string;
  type: string;
  set_identifier: string | null;
  ttl: number | null;
  resource_records: Route53RecordValue[] | null;
  alias_target: Record<string, unknown> | null;
}

/**
 * Load the current mirror row for a record key, if present and not
 * soft-deleted. Supplies the before_value for updates/deletes and,
 * critically, the exact TTL and value set a Route 53 DELETE action requires
 * to match — a DELETE with a mismatched TTL or value set fails or targets
 * the wrong recordset (24-RESEARCH.md Pitfall 3).
 */
export async function loadMirrorRecord(recordKey: string): Promise<MirrorRecordRow | null> {
  const res = await postgresClient.query<RawMirrorRecordRow>(
    `SELECT record_key, zone_id, name, type, set_identifier, ttl, resource_records, alias_target
       FROM route53_records
      WHERE record_key = $1 AND is_deleted = false`,
    [recordKey]
  );
  if (res.rowCount === 0) return null;
  const row = res.rows[0];
  return {
    recordKey: row.record_key,
    zoneId: row.zone_id,
    name: row.name,
    type: row.type,
    setIdentifier: row.set_identifier,
    ttl: row.ttl,
    resourceRecords: row.resource_records,
    aliasTarget: row.alias_target,
  };
}
