import { randomUUID } from 'node:crypto';
import { db } from '../../../lib/db.mjs';
import { PlatformError } from '../../kernel/errors.mjs';

import {
  ACTIVE_DEPLOYMENT_STATES,
  DEPLOYMENT_STATES,
  canTransitionDeployment,
  isActiveDeploymentState,
} from './state-machine.mjs';

function parseJson(value, fallback) {
  try { return value ? JSON.parse(value) : fallback; } catch { return fallback; }
}

function serializeJson(value, fallback) {
  try { return JSON.stringify(value ?? fallback); } catch { return JSON.stringify(fallback); }
}

function normalizeRun(row) {
  if (!row) return null;
  return {
    ...row,
    request: parseJson(row.request_json, {}),
    result: parseJson(row.result_json, {}),
    active: isActiveDeploymentState(row.state),
  };
}

function fail(message, code = 'DEPLOYMENT_STATE_INVALID', status = 409, details = null) {
  throw new PlatformError(message, { code, status, expose: true, details });
}

export function createDeploymentRepository(database = db) {
  const insertRun = database.prepare(`INSERT INTO deployment_runs (
    id, idempotency_key, state, provider, trigger_mode, current_version, target_version,
    build_revision_before, requested_by, reason, github_repository, github_branch,
    request_json, timeout_at, started_at, updated_at
  ) VALUES (?, ?, 'checking', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`);

  const insertEvent = database.prepare(`INSERT INTO deployment_events
    (run_id, state, event_type, message, details_json)
    VALUES (?, ?, ?, ?, ?)`);

  const getRunStmt = database.prepare('SELECT * FROM deployment_runs WHERE id = ?');
  const getIdempotentStmt = database.prepare('SELECT * FROM deployment_runs WHERE idempotency_key = ?');
  const getActiveStmt = database.prepare(`SELECT * FROM deployment_runs
    WHERE state IN (${ACTIVE_DEPLOYMENT_STATES.map(() => '?').join(',')})
    ORDER BY created_at DESC LIMIT 1`);

  function addEvent(runId, state, eventType, message, details = {}) {
    insertEvent.run(runId, state, String(eventType || 'state').slice(0, 80), String(message || '').slice(0, 1000), serializeJson(details, {}));
  }

  function createRun(input) {
    const id = input.id || randomUUID();
    const existing = getIdempotentStmt.get(input.idempotencyKey);
    if (existing) return { run: normalizeRun(existing), reused: true };
    const active = getActiveStmt.get(...ACTIVE_DEPLOYMENT_STATES);
    if (active) fail('Es läuft bereits ein Deployment oder es ist ein Rollback erforderlich.', 'DEPLOYMENT_ALREADY_ACTIVE', 409, { run_id: active.id, state: active.state });

    try {
      insertRun.run(
        id,
        input.idempotencyKey,
        input.provider || 'coolify',
        input.triggerMode || 'webhook',
        input.currentVersion,
        input.targetVersion,
        input.buildRevisionBefore || null,
        input.requestedBy || null,
        input.reason || 'manual',
        input.githubRepository || null,
        input.githubBranch || null,
        serializeJson(input.request || {}, {}),
        input.timeoutAt || null,
      );
    } catch (error) {
      if (String(error?.message || '').includes('UNIQUE')) {
        const raced = getIdempotentStmt.get(input.idempotencyKey);
        if (raced) return { run: normalizeRun(raced), reused: true };
      }
      throw error;
    }
    addEvent(id, 'checking', 'run.created', 'Deployment-Auftrag wurde angelegt.', {
      target_version: input.targetVersion,
      trigger_mode: input.triggerMode,
    });
    return { run: normalizeRun(getRunStmt.get(id)), reused: false };
  }

  function getRun(id) { return normalizeRun(getRunStmt.get(id)); }
  function findByIdempotencyKey(key) { return normalizeRun(getIdempotentStmt.get(key)); }
  function getActiveRun() { return normalizeRun(getActiveStmt.get(...ACTIVE_DEPLOYMENT_STATES)); }

  function listRuns(limit = 25) {
    const safeLimit = Math.max(1, Math.min(200, Number(limit) || 25));
    return database.prepare('SELECT * FROM deployment_runs ORDER BY created_at DESC LIMIT ?').all(safeLimit).map(normalizeRun);
  }

  function listEvents(runId, limit = 200) {
    const safeLimit = Math.max(1, Math.min(1000, Number(limit) || 200));
    return database.prepare(`SELECT * FROM deployment_events WHERE run_id = ? ORDER BY id ASC LIMIT ?`).all(runId, safeLimit)
      .map(row => ({ ...row, details: parseJson(row.details_json, {}) }));
  }

  function transition(runId, nextState, { message = '', eventType = 'state.changed', details = {}, patch = {} } = {}) {
    if (!DEPLOYMENT_STATES.includes(nextState)) fail('Unbekannter Deployment-Zustand.', 'DEPLOYMENT_STATE_UNKNOWN', 500, { next_state: nextState });
    const current = getRunStmt.get(runId);
    if (!current) fail('Deployment-Auftrag wurde nicht gefunden.', 'DEPLOYMENT_RUN_NOT_FOUND', 404);
    if (current.state === nextState) return normalizeRun(current);
    if (!canTransitionDeployment(current.state, nextState)) {
      fail(`Ungültiger Deployment-Übergang von ${current.state} nach ${nextState}.`, 'DEPLOYMENT_TRANSITION_BLOCKED', 409, {
        run_id: runId, from: current.state, to: nextState,
      });
    }

    const completed = ['succeeded', 'failed'].includes(nextState);
    const columns = ['state = ?', "updated_at = datetime('now')"];
    const values = [nextState];
    const allowedPatch = new Set([
      'deployment_id', 'backup_id', 'result_json', 'error_code', 'error_message',
      'build_revision_after', 'timeout_at',
    ]);
    for (const [key, value] of Object.entries(patch || {})) {
      if (!allowedPatch.has(key)) continue;
      columns.push(`${key} = ?`);
      values.push(key.endsWith('_json') ? serializeJson(value, {}) : value);
    }
    if (completed) columns.push("completed_at = datetime('now')");
    values.push(runId);
    database.prepare(`UPDATE deployment_runs SET ${columns.join(', ')} WHERE id = ?`).run(...values);
    addEvent(runId, nextState, eventType, message || `Zustand: ${nextState}`, details);
    return normalizeRun(getRunStmt.get(runId));
  }

  function patchRun(runId, patch = {}, { eventType = null, message = '', details = {} } = {}) {
    const current = getRunStmt.get(runId);
    if (!current) fail('Deployment-Auftrag wurde nicht gefunden.', 'DEPLOYMENT_RUN_NOT_FOUND', 404);
    const allowedPatch = new Set([
      'deployment_id', 'backup_id', 'result_json', 'error_code', 'error_message',
      'build_revision_after', 'timeout_at',
    ]);
    const columns = ["updated_at = datetime('now')"];
    const values = [];
    for (const [key, value] of Object.entries(patch || {})) {
      if (!allowedPatch.has(key)) continue;
      columns.push(`${key} = ?`);
      values.push(key.endsWith('_json') ? serializeJson(value, {}) : value);
    }
    values.push(runId);
    database.prepare(`UPDATE deployment_runs SET ${columns.join(', ')} WHERE id = ?`).run(...values);
    if (eventType) addEvent(runId, current.state, eventType, message || eventType, details);
    return normalizeRun(getRunStmt.get(runId));
  }

  function recordBackupVerification({ backupId, runId = null, status, sha256 = null, fileSize = 0, sqliteIntegrity = null, manifest = {}, errors = [], warnings = [] }) {
    const id = randomUUID();
    database.prepare(`INSERT INTO backup_verifications
      (id, backup_id, deployment_run_id, status, sha256, file_size, sqlite_integrity, manifest_json, errors_json, warnings_json, verified_at)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`).run(
        id, backupId, runId, status, sha256, Number(fileSize) || 0, sqliteIntegrity,
        serializeJson(manifest, {}), serializeJson(errors, []), serializeJson(warnings, []),
      );
    return database.prepare('SELECT * FROM backup_verifications WHERE id = ?').get(id);
  }

  function createProductionCheck({ createdBy = null, results = [] } = {}) {
    const id = randomUUID();
    const passed = results.filter(item => item.status === 'pass').length;
    const warnings = results.filter(item => item.status === 'warning').length;
    const failed = results.filter(item => item.status === 'fail').length;
    const overall = failed ? 'failed' : (warnings ? 'warning' : 'passed');
    database.prepare(`INSERT INTO production_checks
      (id, overall_status, passed_count, warning_count, failed_count, results_json, created_by)
      VALUES (?, ?, ?, ?, ?, ?, ?)`).run(id, overall, passed, warnings, failed, serializeJson(results, []), createdBy);
    return { id, overall_status: overall, passed_count: passed, warning_count: warnings, failed_count: failed, results };
  }

  function latestProductionCheck() {
    const row = database.prepare('SELECT * FROM production_checks ORDER BY created_at DESC LIMIT 1').get();
    return row ? { ...row, results: parseJson(row.results_json, []) } : null;
  }

  return Object.freeze({
    createRun, getRun, findByIdempotencyKey, getActiveRun, listRuns, listEvents,
    transition, patchRun, addEvent, recordBackupVerification,
    createProductionCheck, latestProductionCheck,
  });
}

export const deploymentRepository = createDeploymentRepository();
