import { Prisma, InstanceStatus, AuditAction } from '@prisma/client';
import fs from 'fs/promises';
import { parse as parseDotenv } from 'dotenv';
import { prisma } from '../../lib/prisma';
import { env } from '../../config/env';
import { AppError } from '../../middleware/error-handler';
import { encryptJson, decryptJson } from '../../utils/encryption';
import { generateSecrets } from '../../services/secret-generator';
import { allocatePorts, releasePorts } from '../../services/port-allocator';
import * as docker from '../../services/docker.service';
import { getDriverForInstance, AgentUnreachableError } from '../../services/execution-driver';
import { provision } from './provisioner';
import { CreateInstanceInput, UpdateInstanceInput, RegisterInstanceInput, ReconfigureInstanceInput, ConfigureTunnelInput } from './instances.schemas';
import { buildTemplateContext, renderAllTemplates, clearTemplateCache } from '../../services/template-engine';
import { teardownTunnel } from '../../services/tunnel.service';
import { logger } from '../../utils/logger';
import path from 'path';

// ─── CRUD Operations ─────────────────────────────────────────────────

export async function listInstances() {
  return prisma.instance.findMany({
    orderBy: { createdAt: 'desc' },
    omit: { encryptedSecrets: true, pangolinNewtSecret: true },
    include: {
      portAllocations: true,
      _count: { select: { healthChecks: true, backups: true } },
    },
  });
}

export async function getInstance(id: string) {
  const instance = await prisma.instance.findUnique({
    where: { id },
    omit: { encryptedSecrets: true, pangolinNewtSecret: true },
    include: {
      portAllocations: true,
      healthChecks: { orderBy: { checkedAt: 'desc' }, take: 10 },
      backups: { orderBy: { startedAt: 'desc' }, take: 10 },
    },
  });
  if (!instance) {
    throw new AppError(404, 'Instance not found', 'NOT_FOUND');
  }
  return instance;
}

export async function createInstance(input: CreateInstanceInput, userId: string, ipAddress?: string) {
  // Check uniqueness
  const existing = await prisma.instance.findFirst({
    where: { OR: [{ slug: input.slug }, { domain: input.domain }] },
  });
  if (existing) {
    const field = existing.slug === input.slug ? 'slug' : 'domain';
    throw new AppError(409, `Instance with this ${field} already exists`, 'DUPLICATE');
  }

  // Allocate ports
  const ports = await allocatePorts();

  // Generate secrets
  const secrets = generateSecrets(input.adminEmail);

  // Compute paths
  const composeProject = `cml-${input.slug}`;
  const basePath = path.join(env.INSTANCES_BASE_PATH, input.slug, 'changemaker.lite');

  // Create instance record
  const instance = await prisma.instance.create({
    data: {
      slug: input.slug,
      name: input.name,
      domain: input.domain,
      status: InstanceStatus.PROVISIONING,
      basePath,
      composeProject,
      gitBranch: env.CML_GIT_BRANCH,
      portConfig: ports.config,
      encryptedSecrets: encryptJson(secrets as unknown as Record<string, unknown>),
      enableMedia: input.enableMedia,
      enableChat: input.enableChat,
      enableGancio: input.enableGancio,
      enableListmonk: input.enableListmonk,
      enableMonitoring: input.enableMonitoring,
      enableDevTools: input.enableDevTools,
      enablePayments: input.enablePayments,
      enableMeet: input.enableMeet,
      enableSms: input.enableSms,
      enableSocial: input.enableSocial,
      enablePeople: input.enablePeople,
      enableAnalytics: input.enableAnalytics,
      jvbAdvertiseIp: input.jvbAdvertiseIp,
      adminEmail: input.adminEmail,
      pangolinEndpoint: input.enablePangolin ? input.pangolinEndpoint : null,
      pangolinNewtId: input.enablePangolin ? input.pangolinNewtId : null,
      pangolinNewtSecret: input.enablePangolin ? input.pangolinNewtSecret : null,
      smtpHost: input.smtpHost,
      smtpPort: input.smtpPort,
      smtpUser: input.smtpUser,
      smtpFrom: input.smtpFrom,
      emailTestMode: input.emailTestMode,
      notes: input.notes,
      portAllocations: {
        create: ports.allocations,
      },
    },
    include: { portAllocations: true },
  });

  // Audit log
  await prisma.auditLog.create({
    data: {
      userId,
      instanceId: instance.id,
      action: AuditAction.INSTANCE_CREATE,
      details: { name: input.name, domain: input.domain, slug: input.slug },
      ipAddress,
    },
  });

  // Kick off provisioning asynchronously (fire-and-forget)
  provision(instance.id).catch((err) => {
    logger.error(`[instances] Provisioning failed for ${instance.slug}: ${err}`);
  });

  return instance;
}

export async function registerInstance(input: RegisterInstanceInput, userId: string, ipAddress?: string) {
  // Check uniqueness (slug, domain, composeProject)
  const existing = await prisma.instance.findFirst({
    where: {
      OR: [
        { slug: input.slug },
        { domain: input.domain },
        { composeProject: input.composeProject },
      ],
    },
  });
  if (existing) {
    const field = existing.slug === input.slug ? 'slug'
      : existing.domain === input.domain ? 'domain'
      : 'composeProject';
    throw new AppError(409, `Instance with this ${field} already exists`, 'DUPLICATE');
  }

  // Verify basePath has a docker-compose.yml
  try {
    await fs.access(path.join(input.basePath, 'docker-compose.yml'));
  } catch {
    throw new AppError(400, `No docker-compose.yml found at ${input.basePath}`, 'INVALID_PATH');
  }

  // Detect running containers to determine initial status
  let initialStatus: InstanceStatus = InstanceStatus.STOPPED;
  try {
    const containers = await docker.composePs(input.basePath, input.composeProject);
    const runningCount = containers.filter((c) => c.state === 'running').length;
    if (runningCount > 0) {
      initialStatus = InstanceStatus.RUNNING;
    }
  } catch {
    logger.warn(`[instances] Could not detect containers for ${input.composeProject}, defaulting to STOPPED`);
  }

  // Create instance record
  const instance = await prisma.instance.create({
    data: {
      slug: input.slug,
      name: input.name,
      domain: input.domain,
      status: initialStatus,
      statusMessage: initialStatus === InstanceStatus.RUNNING ? 'Registered — containers running' : 'Registered — containers not running',
      basePath: input.basePath,
      composeProject: input.composeProject,
      portConfig: input.portConfig,
      encryptedSecrets: null,
      isRegistered: true,
      enableMedia: input.enableMedia,
      enableChat: input.enableChat,
      enableGancio: input.enableGancio,
      enableListmonk: input.enableListmonk,
      enableMonitoring: input.enableMonitoring,
      enableDevTools: input.enableDevTools,
      enablePayments: input.enablePayments,
      enableMeet: input.enableMeet,
      enableSms: input.enableSms,
      enableSocial: input.enableSocial,
      enablePeople: input.enablePeople,
      enableAnalytics: input.enableAnalytics,
      adminEmail: input.adminEmail,
      notes: input.notes,
    },
  });

  // Create PortAllocation records (try-catch each for unique constraint)
  for (const [service, port] of Object.entries(input.portConfig)) {
    try {
      await prisma.portAllocation.create({
        data: { port, service, instanceId: instance.id },
      });
    } catch (err) {
      logger.warn(`[instances] Port ${port} (${service}) already allocated, skipping`);
    }
  }

  // Audit log
  await prisma.auditLog.create({
    data: {
      userId,
      instanceId: instance.id,
      action: AuditAction.INSTANCE_CREATE,
      details: { name: input.name, domain: input.domain, slug: input.slug, registered: true },
      ipAddress,
    },
  });

  // Trigger immediate health check if running
  if (initialStatus === InstanceStatus.RUNNING) {
    import('../../services/health.service').then((healthService) => {
      healthService.checkInstanceHealth(instance.id).catch((err) => {
        logger.warn(`[instances] Initial health check failed for ${instance.slug}: ${(err as Error).message}`);
      });
    });
  }

  // Re-fetch with relations
  return prisma.instance.findUnique({
    where: { id: instance.id },
    include: { portAllocations: true },
  });
}

export async function updateInstance(id: string, input: UpdateInstanceInput, userId: string, ipAddress?: string) {
  const instance = await prisma.instance.findUnique({ where: { id } });
  if (!instance) {
    throw new AppError(404, 'Instance not found', 'NOT_FOUND');
  }

  const updated = await prisma.instance.update({
    where: { id },
    data: input,
  });

  await prisma.auditLog.create({
    data: {
      userId,
      instanceId: id,
      action: AuditAction.INSTANCE_UPDATE,
      details: input as unknown as Prisma.InputJsonValue,
      ipAddress,
    },
  });

  return updated;
}

export async function deleteInstance(id: string, userId: string, ipAddress?: string) {
  const instance = await prisma.instance.findUnique({ where: { id } });
  if (!instance) {
    throw new AppError(404, 'Instance not found', 'NOT_FOUND');
  }

  // Registered instances: just remove from DB, never touch containers or files
  if (instance.isRegistered) {
    await releasePorts(id);
    await prisma.instance.delete({ where: { id } });

    await prisma.auditLog.create({
      data: {
        userId,
        action: AuditAction.INSTANCE_DELETE,
        details: { name: instance.name, domain: instance.domain, slug: instance.slug, unregistered: true },
        ipAddress,
      },
    });

    return { message: 'Instance unregistered' };
  }

  // Mark as destroying
  await prisma.instance.update({
    where: { id },
    data: { status: InstanceStatus.DESTROYING, statusMessage: 'Shutting down containers...' },
  });

  // Tear down Pangolin site + resources first. If we crashed after composeDown
  // but before this, the Pangolin entities would leak for the lifetime of the org.
  // Best effort — matches the Docker-cleanup tolerance below.
  if (instance.pangolinSiteId) {
    try {
      await teardownTunnel(id, userId, ipAddress ?? null);
      logger.info(`[instances] ${instance.slug}: Pangolin tunnel torn down`);
    } catch (err) {
      logger.warn(`[instances] ${instance.slug}: Pangolin teardown warning: ${(err as Error).message}`);
    }
  }

  // Stop containers and remove volumes
  try {
    const driver = await getDriverForInstance(instance);
    await driver.composeDown(instance.basePath, instance.composeProject, true);
    logger.info(`[instances] ${instance.slug}: Containers stopped and volumes removed`);
  } catch (err) {
    logger.warn(`[instances] ${instance.slug}: Docker cleanup warning: ${(err as Error).message}`);
    // Continue with deletion even if docker cleanup partially fails
  }

  // Delete instance directory (with safety check)
  const instanceDir = path.resolve(path.dirname(instance.basePath));
  const expectedBase = path.resolve(env.INSTANCES_BASE_PATH);
  if (instanceDir.startsWith(expectedBase + '/') && instanceDir !== expectedBase) {
    try {
      await fs.rm(instanceDir, { recursive: true, force: true });
      logger.info(`[instances] ${instance.slug}: Directory ${instanceDir} removed`);
    } catch (err) {
      logger.warn(`[instances] ${instance.slug}: Directory cleanup warning: ${(err as Error).message}`);
    }
  } else {
    logger.error(`[instances] ${instance.slug}: Refusing to delete path outside base: ${instanceDir}`);
  }

  // Release ports and delete instance from DB
  await releasePorts(id);
  await prisma.instance.delete({ where: { id } });

  await prisma.auditLog.create({
    data: {
      userId,
      action: AuditAction.INSTANCE_DELETE,
      details: { name: instance.name, domain: instance.domain, slug: instance.slug },
      ipAddress,
    },
  });

  return { message: 'Instance deleted' };
}

export async function getInstanceSecrets(id: string) {
  const instance = await prisma.instance.findUnique({ where: { id } });
  if (!instance) {
    throw new AppError(404, 'Instance not found', 'NOT_FOUND');
  }

  // CCP-provisioned instance: decrypt from DB
  if (!instance.isRegistered && instance.encryptedSecrets) {
    return decryptJson(instance.encryptedSecrets);
  }

  // Registered/discovered instance: read from .env on disk
  // Path traversal protection: basePath must be within INSTANCES_BASE_PATH or CML_SOURCE_PATH
  const resolvedBase = path.resolve(instance.basePath);
  const allowedPaths = [
    path.resolve(env.INSTANCES_BASE_PATH),
    ...(env.CML_SOURCE_PATH ? [path.resolve(env.CML_SOURCE_PATH)] : []),
  ];
  const isAllowed = allowedPaths.some(
    (allowed) => resolvedBase === allowed || resolvedBase.startsWith(allowed + '/')
  );
  if (!isAllowed) {
    throw new AppError(400, 'Instance path is outside the allowed directory', 'INVALID_PATH');
  }

  const envPath = path.join(resolvedBase, '.env');
  let envVars: Record<string, string> | null = null;
  try {
    const content = await fs.readFile(envPath, 'utf-8');
    envVars = parseDotenv(Buffer.from(content));
  } catch {
    envVars = null;
  }

  if (!envVars) {
    throw new AppError(400, 'Could not read .env file for this instance', 'ENV_NOT_FOUND');
  }

  return {
    initialAdminEmail: envVars.INITIAL_ADMIN_EMAIL || instance.adminEmail,
    initialAdminPassword: envVars.INITIAL_ADMIN_PASSWORD || null,
  };
}

// ─── Lifecycle Operations ────────────────────────────────────────────

export async function provisionInstance(id: string, userId: string, ipAddress?: string) {
  // Registered instances cannot be provisioned
  const check = await prisma.instance.findUnique({ where: { id }, select: { isRegistered: true } });
  if (check?.isRegistered) {
    throw new AppError(400, 'Cannot provision a registered instance', 'NOT_MANAGED');
  }

  // Atomic check-and-update to prevent concurrent provisioning
  const { count } = await prisma.instance.updateMany({
    where: { id, status: { in: [InstanceStatus.ERROR, InstanceStatus.STOPPED] } },
    data: { status: InstanceStatus.PROVISIONING, statusMessage: 'Retrying provisioning...' },
  });

  if (count === 0) {
    const instance = await prisma.instance.findUnique({ where: { id } });
    if (!instance) throw new AppError(404, 'Instance not found', 'NOT_FOUND');
    throw new AppError(400, `Cannot provision instance in ${instance.status} state`, 'INVALID_STATE');
  }

  await prisma.auditLog.create({
    data: {
      userId,
      instanceId: id,
      action: AuditAction.INSTANCE_UPDATE,
      details: { event: 'provision_retry' },
      ipAddress,
    },
  });

  // Fire-and-forget
  provision(id).catch((err) => {
    logger.error(`[instances] Re-provisioning failed for ${id}: ${err}`);
  });

  return { message: 'Provisioning started' };
}

export async function startInstance(id: string, userId: string, ipAddress?: string) {
  const instance = await prisma.instance.findUnique({ where: { id } });
  if (!instance) {
    throw new AppError(404, 'Instance not found', 'NOT_FOUND');
  }

  if (instance.status !== 'STOPPED' && instance.status !== 'ERROR') {
    throw new AppError(400, `Cannot start instance in ${instance.status} state`, 'INVALID_STATE');
  }

  try {
    const driver = await getDriverForInstance(instance);
    await driver.composeUp(instance.basePath, instance.composeProject);

    await prisma.instance.update({
      where: { id },
      data: { status: InstanceStatus.RUNNING, statusMessage: 'All containers started' },
    });

    await prisma.auditLog.create({
      data: {
        userId,
        instanceId: id,
        action: AuditAction.INSTANCE_START,
        details: { slug: instance.slug },
        ipAddress,
      },
    });

    return { message: 'Instance started' };
  } catch (err) {
    if (err instanceof AgentUnreachableError) {
      await prisma.instance.update({
        where: { id },
        data: { status: InstanceStatus.ERROR, statusMessage: `Agent unreachable: ${err.agentUrl}` },
      });
      throw new AppError(503, err.message, 'AGENT_UNREACHABLE');
    }
    const errorMsg = (err as Error).message;
    await prisma.instance.update({
      where: { id },
      data: { status: InstanceStatus.ERROR, statusMessage: `Start failed: ${errorMsg}` },
    });
    throw new AppError(500, `Failed to start instance: ${errorMsg}`, 'DOCKER_ERROR');
  }
}

export async function stopInstance(id: string, userId: string, ipAddress?: string) {
  const instance = await prisma.instance.findUnique({ where: { id } });
  if (!instance) {
    throw new AppError(404, 'Instance not found', 'NOT_FOUND');
  }

  if (instance.status !== 'RUNNING' && instance.status !== 'ERROR') {
    throw new AppError(400, `Cannot stop instance in ${instance.status} state`, 'INVALID_STATE');
  }

  try {
    const driver = await getDriverForInstance(instance);
    await driver.composeStop(instance.basePath, instance.composeProject);

    await prisma.instance.update({
      where: { id },
      data: { status: InstanceStatus.STOPPED, statusMessage: 'All containers stopped' },
    });

    await prisma.auditLog.create({
      data: {
        userId,
        instanceId: id,
        action: AuditAction.INSTANCE_STOP,
        details: { slug: instance.slug },
        ipAddress,
      },
    });

    return { message: 'Instance stopped' };
  } catch (err) {
    if (err instanceof AgentUnreachableError) {
      throw new AppError(503, err.message, 'AGENT_UNREACHABLE');
    }
    const errorMsg = (err as Error).message;
    throw new AppError(500, `Failed to stop instance: ${errorMsg}`, 'DOCKER_ERROR');
  }
}

export async function restartInstance(id: string, userId: string, ipAddress?: string, service?: string) {
  const instance = await prisma.instance.findUnique({ where: { id } });
  if (!instance) {
    throw new AppError(404, 'Instance not found', 'NOT_FOUND');
  }

  try {
    const driver = await getDriverForInstance(instance);
    await driver.composeRestart(instance.basePath, instance.composeProject, service);

    await prisma.auditLog.create({
      data: {
        userId,
        instanceId: id,
        action: AuditAction.INSTANCE_RESTART,
        details: { slug: instance.slug, service: service || 'all' },
        ipAddress,
      },
    });

    return { message: `${service || 'All services'} restarted` };
  } catch (err) {
    if (err instanceof AgentUnreachableError) {
      throw new AppError(503, err.message, 'AGENT_UNREACHABLE');
    }
    const errorMsg = (err as Error).message;
    throw new AppError(500, `Failed to restart: ${errorMsg}`, 'DOCKER_ERROR');
  }
}

export async function getInstanceServices(id: string) {
  const instance = await prisma.instance.findUnique({ where: { id } });
  if (!instance) {
    throw new AppError(404, 'Instance not found', 'NOT_FOUND');
  }

  try {
    const driver = await getDriverForInstance(instance);
    return await driver.composePs(instance.basePath, instance.composeProject);
  } catch {
    // If compose ps fails (e.g. no containers or agent unreachable), return empty array
    return [];
  }
}

export async function getInstanceLogs(
  id: string,
  service?: string,
  tail = 200,
  since?: string
) {
  const instance = await prisma.instance.findUnique({ where: { id } });
  if (!instance) {
    throw new AppError(404, 'Instance not found', 'NOT_FOUND');
  }

  try {
    const driver = await getDriverForInstance(instance);
    return await driver.composeLogs(
      instance.basePath,
      instance.composeProject,
      service,
      tail,
      since
    );
  } catch (err) {
    if (err instanceof AgentUnreachableError) {
      throw new AppError(503, err.message, 'AGENT_UNREACHABLE');
    }
    throw new AppError(500, `Failed to get logs: ${(err as Error).message}`, 'DOCKER_ERROR');
  }
}

// ─── Reconfiguration ─────────────────────────────────────────────────

export async function reconfigureInstance(
  id: string,
  features: ReconfigureInstanceInput,
  userId: string,
  ipAddress?: string
) {
  const instance = await prisma.instance.findUnique({ where: { id } });
  if (!instance) {
    throw new AppError(404, 'Instance not found', 'NOT_FOUND');
  }
  if (instance.isRegistered) {
    throw new AppError(400, 'Cannot reconfigure an external instance', 'NOT_MANAGED');
  }
  if (!instance.encryptedSecrets) {
    throw new AppError(400, 'Instance has no secrets — cannot reconfigure', 'NOT_MANAGED');
  }
  if (instance.status !== 'RUNNING' && instance.status !== 'STOPPED') {
    throw new AppError(400, `Cannot reconfigure instance in ${instance.status} state`, 'INVALID_STATE');
  }

  // Update feature flags in DB
  const updated = await prisma.instance.update({
    where: { id },
    data: {
      ...features,
      statusMessage: 'Reconfiguring...',
    },
  });

  // Clear template cache so updated templates are re-read
  clearTemplateCache();

  // Re-render templates with updated flags
  const secrets = decryptJson<Record<string, string>>(instance.encryptedSecrets);
  const context = buildTemplateContext(updated, secrets);

  const driver = await getDriverForInstance(instance);
  if (instance.isRemote) {
    // Remote: render in memory, send files to agent
    const { renderAllTemplatesInMemory } = await import('../../services/template-engine');
    const files = await renderAllTemplatesInMemory(context);
    await driver.writeFiles(instance.basePath, files);
  } else {
    await renderAllTemplates(context, instance.basePath);
  }

  // If instance is running, apply changes via docker compose up
  if (instance.status === 'RUNNING') {
    try {
      await driver.composeUp(instance.basePath, instance.composeProject);
      // --remove-orphans (from composeUp) will clean up disabled services

      await prisma.instance.update({
        where: { id },
        data: { statusMessage: 'Reconfiguration complete' },
      });
    } catch (err) {
      if (err instanceof AgentUnreachableError) {
        await prisma.instance.update({
          where: { id },
          data: { statusMessage: `Agent unreachable: ${(err as AgentUnreachableError).agentUrl}` },
        });
        throw new AppError(503, err.message, 'AGENT_UNREACHABLE');
      }
      const errorMsg = (err as Error).message;
      await prisma.instance.update({
        where: { id },
        data: { statusMessage: `Reconfiguration failed: ${errorMsg}` },
      });
      throw new AppError(500, `Reconfiguration failed: ${errorMsg}`, 'DOCKER_ERROR');
    }
  } else {
    await prisma.instance.update({
      where: { id },
      data: { statusMessage: 'Reconfiguration complete — start instance to apply' },
    });
  }

  // Audit log
  await prisma.auditLog.create({
    data: {
      userId,
      instanceId: id,
      action: AuditAction.INSTANCE_UPDATE,
      details: { event: 'reconfigure', features } as unknown as Prisma.InputJsonValue,
      ipAddress,
    },
  });

  return updated;
}

// ─── Tunnel Management ──────────────────────────────────────────────

export async function configureTunnel(
  id: string,
  input: ConfigureTunnelInput,
  userId: string,
  ipAddress?: string
) {
  const instance = await prisma.instance.findUnique({ where: { id } });
  if (!instance) {
    throw new AppError(404, 'Instance not found', 'NOT_FOUND');
  }
  if (instance.isRegistered) {
    throw new AppError(400, 'Cannot configure tunnel on an external instance', 'NOT_MANAGED');
  }
  if (!instance.encryptedSecrets) {
    throw new AppError(400, 'Instance has no secrets — cannot configure tunnel', 'NOT_MANAGED');
  }
  if (instance.status !== 'RUNNING' && instance.status !== 'STOPPED') {
    throw new AppError(400, `Cannot configure tunnel in ${instance.status} state`, 'INVALID_STATE');
  }

  // If secret not provided, keep existing; throw if both are null (first-time setup)
  const newtSecret = input.pangolinNewtSecret || instance.pangolinNewtSecret;
  if (!newtSecret) {
    throw new AppError(400, 'Newt secret is required for first-time tunnel setup', 'VALIDATION_ERROR');
  }

  // Update DB
  const updated = await prisma.instance.update({
    where: { id },
    data: {
      pangolinEndpoint: input.pangolinEndpoint,
      pangolinNewtId: input.pangolinNewtId,
      pangolinNewtSecret: newtSecret,
      statusMessage: 'Configuring tunnel...',
    },
  });

  // Re-render templates
  clearTemplateCache();
  const secrets = decryptJson<Record<string, string>>(instance.encryptedSecrets);
  const context = buildTemplateContext(updated, secrets);

  const driver = await getDriverForInstance(instance);
  if (instance.isRemote) {
    const { renderAllTemplatesInMemory } = await import('../../services/template-engine');
    const files = await renderAllTemplatesInMemory(context);
    await driver.writeFiles(instance.basePath, files);
  } else {
    await renderAllTemplates(context, instance.basePath);
  }

  // If running, bring up the newt container
  if (instance.status === 'RUNNING') {
    try {
      await driver.composeUp(instance.basePath, instance.composeProject, ['newt']);
      await prisma.instance.update({
        where: { id },
        data: { statusMessage: 'Tunnel configured and Newt started' },
      });
    } catch (err) {
      const errorMsg = (err as Error).message;
      await prisma.instance.update({
        where: { id },
        data: { statusMessage: `Tunnel configured but Newt failed: ${errorMsg}` },
      });
      throw new AppError(500, `Tunnel configured but Newt failed to start: ${errorMsg}`, 'DOCKER_ERROR');
    }
  } else {
    await prisma.instance.update({
      where: { id },
      data: { statusMessage: 'Tunnel configured — start instance to apply' },
    });
  }

  // Audit log
  await prisma.auditLog.create({
    data: {
      userId,
      instanceId: id,
      action: AuditAction.PANGOLIN_SETUP,
      details: {
        event: 'tunnel_configure',
        endpoint: input.pangolinEndpoint,
        newtId: input.pangolinNewtId,
      } as unknown as Prisma.InputJsonValue,
      ipAddress,
    },
  });

  return updated;
}

export async function removeTunnel(
  id: string,
  userId: string,
  ipAddress?: string
) {
  const instance = await prisma.instance.findUnique({ where: { id } });
  if (!instance) {
    throw new AppError(404, 'Instance not found', 'NOT_FOUND');
  }
  if (instance.isRegistered) {
    throw new AppError(400, 'Cannot modify tunnel on an external instance', 'NOT_MANAGED');
  }
  if (!instance.encryptedSecrets) {
    throw new AppError(400, 'Instance has no secrets — cannot modify tunnel', 'NOT_MANAGED');
  }
  if (instance.status !== 'RUNNING' && instance.status !== 'STOPPED') {
    throw new AppError(400, `Cannot remove tunnel in ${instance.status} state`, 'INVALID_STATE');
  }

  // Null out all Pangolin fields
  const updated = await prisma.instance.update({
    where: { id },
    data: {
      pangolinEndpoint: null,
      pangolinNewtId: null,
      pangolinNewtSecret: null,
      statusMessage: 'Removing tunnel...',
    },
  });

  // Re-render templates (enablePangolin will compute to false, removing newt block)
  clearTemplateCache();
  const secrets = decryptJson<Record<string, string>>(instance.encryptedSecrets);
  const context = buildTemplateContext(updated, secrets);

  const driver = await getDriverForInstance(instance);
  if (instance.isRemote) {
    const { renderAllTemplatesInMemory } = await import('../../services/template-engine');
    const files = await renderAllTemplatesInMemory(context);
    await driver.writeFiles(instance.basePath, files);
  } else {
    await renderAllTemplates(context, instance.basePath);
  }

  // If running, full compose up with --remove-orphans removes the orphaned newt container
  if (instance.status === 'RUNNING') {
    try {
      await driver.composeUp(instance.basePath, instance.composeProject);
      await prisma.instance.update({
        where: { id },
        data: { statusMessage: 'Tunnel removed' },
      });
    } catch (err) {
      const errorMsg = (err as Error).message;
      await prisma.instance.update({
        where: { id },
        data: { statusMessage: `Tunnel removed but Docker update failed: ${errorMsg}` },
      });
    }
  } else {
    await prisma.instance.update({
      where: { id },
      data: { statusMessage: 'Tunnel removed — start instance to apply' },
    });
  }

  // Audit log
  await prisma.auditLog.create({
    data: {
      userId,
      instanceId: id,
      action: AuditAction.PANGOLIN_SETUP,
      details: { event: 'tunnel_remove' } as unknown as Prisma.InputJsonValue,
      ipAddress,
    },
  });

  return updated;
}
