import { InstanceStatus, AuditAction } from '@prisma/client';
import { execFile as execFileCb } from 'child_process';
import { promisify } from 'util';
import fs from 'fs/promises';
import path from 'path';
import { prisma } from '../../lib/prisma';
import { env } from '../../config/env';
import { decryptJson } from '../../utils/encryption';

import { renderAllTemplates, buildTemplateContext } from '../../services/template-engine';
import * as docker from '../../services/docker.service';
import { logger } from '../../utils/logger';
import { createEvent } from '../../services/event.service';
const execFile = promisify(execFileCb);

/**
 * Directories/files to exclude when copying CML source to instance directory.
 */
const COPY_EXCLUDES = [
  'node_modules',
  '.git',
  '.env',
  'changemaker-control-panel',
  '.claude',
];

/**
 * Update instance status and statusMessage in the database.
 */
async function updateStatus(
  instanceId: string,
  status: InstanceStatus,
  statusMessage: string
): Promise<void> {
  await prisma.instance.update({
    where: { id: instanceId },
    data: { status, statusMessage },
  });
}

/**
 * Provision a CML instance: copy source, render configs, build and start Docker stack.
 *
 * This function runs asynchronously — call it without awaiting.
 * Progress is tracked via instance.status and instance.statusMessage.
 */
export async function provision(instanceId: string): Promise<void> {
  const totalSteps = 13;
  let step = 0;

  function stepMsg(description: string): string {
    step++;
    return `Step ${step}/${totalSteps}: ${description}`;
  }

  try {
    // Load instance with all details
    const instance = await prisma.instance.findUnique({
      where: { id: instanceId },
      include: { portAllocations: true },
    });
    if (!instance) {
      throw new Error(`Instance ${instanceId} not found`);
    }

    const { basePath, composeProject } = instance;
    const instanceDir = path.dirname(basePath); // parent of changemaker.lite

    // ── Step 1: Create instance directory ───────────────────────────
    await updateStatus(instanceId, InstanceStatus.PROVISIONING, stepMsg('Creating instance directory'));
    logger.info(`[provisioner] ${instance.slug}: Creating directory ${basePath}`);
    await fs.mkdir(basePath, { recursive: true });

    // ── Step 2: Copy CML source ────────────────────────────────────
    await updateStatus(instanceId, InstanceStatus.PROVISIONING, stepMsg('Copying CML source'));
    logger.info(`[provisioner] ${instance.slug}: Copying from ${env.CML_SOURCE_PATH} to ${basePath}`);

    const rsyncArgs = ['-a', ...COPY_EXCLUDES.flatMap((e) => ['--exclude', e]), `${env.CML_SOURCE_PATH}/`, `${basePath}/`];
    await execFile('rsync', rsyncArgs, { timeout: 120_000 });

    // ── Step 2b: Create media directories ──────────────────────────
    // Media API volume mounts use ./media as the read-only base with
    // rw overlays for subdirectories. All must exist before Docker starts.
    const mediaDirs = [
      'media/local/inbox',
      'media/local/thumbnails',
      'media/local/photos',
      'media/public',
    ];
    for (const dir of mediaDirs) {
      await fs.mkdir(path.join(basePath, dir), { recursive: true });
    }
    logger.info(`[provisioner] ${instance.slug}: Created media directories`);

    // ── Step 3: Decrypt secrets ────────────────────────────────────
    await updateStatus(instanceId, InstanceStatus.PROVISIONING, stepMsg('Preparing configuration'));
    if (!instance.encryptedSecrets) {
      throw new Error('Instance has no encrypted secrets — cannot provision');
    }
    const secrets = decryptJson<Record<string, string>>(instance.encryptedSecrets);

    // ── Step 4: Build template context ─────────────────────────────
    const context = buildTemplateContext(instance, secrets);

    // ── Step 5: Render templates ───────────────────────────────────
    await updateStatus(instanceId, InstanceStatus.PROVISIONING, stepMsg('Rendering configuration files'));
    logger.info(`[provisioner] ${instance.slug}: Rendering templates to ${basePath}`);
    await renderAllTemplates(context, basePath);

    // ── Step 6: Pull base images ───────────────────────────────────
    await updateStatus(instanceId, InstanceStatus.PROVISIONING, stepMsg('Pulling Docker images'));
    logger.info(`[provisioner] ${instance.slug}: Pulling images`);
    try {
      await docker.composePull(basePath, composeProject);
    } catch (err) {
      // Pull failures are non-fatal if images already exist locally
      logger.warn(`[provisioner] ${instance.slug}: Pull warning: ${(err as Error).message}`);
    }

    // ── Step 7: Build custom images (skipped in registry mode) ─────
    if (env.USE_REGISTRY_IMAGES) {
      step++; // Advance step counter but skip the build
      logger.info(`[provisioner] ${instance.slug}: Skipping build (registry mode — images pulled from ${env.GITEA_REGISTRY})`);
    } else {
      await updateStatus(instanceId, InstanceStatus.PROVISIONING, stepMsg('Building Docker images'));
      logger.info(`[provisioner] ${instance.slug}: Building images`);
      await docker.composeBuild(basePath, composeProject);
    }

    // ── Step 8: Start infrastructure (Postgres + Redis) ────────────
    await updateStatus(instanceId, InstanceStatus.PROVISIONING, stepMsg('Starting database and cache'));
    logger.info(`[provisioner] ${instance.slug}: Starting infrastructure services`);
    await docker.composeUp(basePath, composeProject, ['v2-postgres', 'redis']);

    // ── Step 9: Wait for infrastructure healthy ────────────────────
    await updateStatus(instanceId, InstanceStatus.PROVISIONING, stepMsg('Waiting for database to be ready'));
    logger.info(`[provisioner] ${instance.slug}: Waiting for Postgres + Redis healthy`);
    // Container names match template's container_name: {{containerPrefix}}-postgres / {{containerPrefix}}-redis
    const pgContainer = `${composeProject}-postgres`;
    const redisContainer = `${composeProject}-redis`;
    await Promise.all([
      docker.waitForHealthy(pgContainer, 60_000),
      docker.waitForHealthy(redisContainer, 60_000),
    ]);

    // ── Step 10: Start all services ────────────────────────────────
    // The API entrypoint (docker-entrypoint.sh) handles:
    //   1. Wait for Postgres
    //   2. prisma migrate deploy
    //   3. prisma db seed (needs tsx — installed in production image)
    //   4. Start the server
    await updateStatus(instanceId, InstanceStatus.PROVISIONING, stepMsg('Starting all services'));
    logger.info(`[provisioner] ${instance.slug}: Starting all services (entrypoint handles migrate + seed)`);
    await docker.composeUp(basePath, composeProject);

    // ── Step 11: Wait for API healthy ───────────────────────────────
    await updateStatus(instanceId, InstanceStatus.PROVISIONING, stepMsg('Waiting for API to be ready'));
    logger.info(`[provisioner] ${instance.slug}: Waiting for API health check`);
    const ports = instance.portConfig as Record<string, number>;
    await docker.waitForHttp(`http://host.docker.internal:${ports.api}/api/health`, 180_000);

    // ── Step 12: Verify instance health ─────────────────────────────
    await updateStatus(instanceId, InstanceStatus.PROVISIONING, stepMsg('Verifying instance health'));
    logger.info(`[provisioner] ${instance.slug}: Final health verification`);
    await docker.waitForHttp(`http://host.docker.internal:${ports.api}/api/health`, 30_000);

    // ── Step 13: done (below) ───────────────────────────────────────

    // ── Done! ──────────────────────────────────────────────────────
    await updateStatus(instanceId, InstanceStatus.RUNNING, 'Provisioning complete');
    logger.info(`[provisioner] ${instance.slug}: Provisioning complete!`);

    // Audit log
    await prisma.auditLog.create({
      data: {
        instanceId,
        action: AuditAction.INSTANCE_CREATE,
        details: { event: 'provisioning_complete', slug: instance.slug },
      },
    });
  } catch (err) {
    const errorMsg = err instanceof Error ? err.message : String(err);
    logger.error(`[provisioner] ${instanceId}: Failed — ${errorMsg}`);
    await updateStatus(
      instanceId,
      InstanceStatus.ERROR,
      `Provisioning failed at ${step > 0 ? `step ${step}/${totalSteps}` : 'startup'}: ${errorMsg}`
    ).catch(() => {});

    // Audit log
    await prisma.auditLog.create({
      data: {
        instanceId,
        action: AuditAction.INSTANCE_CREATE,
        details: { event: 'provisioning_failed', error: errorMsg, step },
      },
    }).catch(() => {});

    // Create error event
    await createEvent(
      instanceId,
      'ERROR',
      'provisioning',
      'Provisioning failed',
      `Failed at step ${step}/${totalSteps}: ${errorMsg.slice(0, 500)}`,
      { step, totalSteps }
    ).catch(() => {});
  }
}
