/**
 * Preview Manager
 *
 * Orchestrates preview deployment lifecycle: start, stop, list, status.
 * Uses a shared gateway (single Caddy container) for subdomain-based routing.
 * Docker is the source of truth — no database persistence.
 */

import { join } from 'node:path';
import { mkdir, writeFile, rm } from 'node:fs/promises';
import { customAlphabet } from 'nanoid';
import type { ProjectRepository } from '../db/repositories/project-repository.js';
import type { PhaseRepository } from '../db/repositories/phase-repository.js';
import type { InitiativeRepository } from '../db/repositories/initiative-repository.js';
import type { EventBus } from '../events/types.js';
import type {
  PreviewStatus,
  StartPreviewOptions,
  PreviewConfig,
} from './types.js';
import { COMPOSE_PROJECT_PREFIX, PREVIEW_LABELS } from './types.js';
import { discoverConfig } from './config-reader.js';
import { generateComposeFile, generateLabels } from './compose-generator.js';
import {
  isDockerAvailable,
  composeUp,
  composeDown,
  execInContainer,
  composePs,
  listPreviewProjects,
  getContainerLabels,
} from './docker-client.js';
import { waitForHealthy } from './health-checker.js';
import { GatewayManager } from './gateway.js';
import type { GatewayRoute } from './gateway.js';
import { createPreviewWorktree, removePreviewWorktree } from './worktree.js';
import { getProjectCloneDir } from '../git/project-clones.js';
import { phaseBranchName } from '../git/branch-naming.js';
import { createModuleLogger } from '../logger/index.js';
import type {
  PreviewBuildingEvent,
  PreviewReadyEvent,
  PreviewStoppedEvent,
  PreviewFailedEvent,
  PhasePendingReviewEvent,
  AgentStoppedEvent,
} from '../events/types.js';
import type { AgentManager } from '../agent/types.js';

const log = createModuleLogger('preview');

/** Directory for preview deployment artifacts (relative to workspace root) */
const PREVIEWS_DIR = '.cw-previews';

export class PreviewManager {
  private readonly projectRepository: ProjectRepository;
  private readonly eventBus: EventBus;
  private readonly workspaceRoot: string;
  private readonly phaseRepository: PhaseRepository;
  private readonly initiativeRepository: InitiativeRepository;
  private readonly gatewayManager: GatewayManager;

  /** In-memory tracking of active preview routes for fast Caddyfile regeneration */
  private readonly activeRoutes = new Map<string, GatewayRoute[]>();

  constructor(
    projectRepository: ProjectRepository,
    eventBus: EventBus,
    workspaceRoot: string,
    phaseRepository: PhaseRepository,
    initiativeRepository: InitiativeRepository,
  ) {
    this.projectRepository = projectRepository;
    this.eventBus = eventBus;
    this.workspaceRoot = workspaceRoot;
    this.phaseRepository = phaseRepository;
    this.initiativeRepository = initiativeRepository;
    this.gatewayManager = new GatewayManager(workspaceRoot);

    this.setupEventListeners();
  }

  /**
   * Start a preview deployment.
   *
   * 1. Check Docker availability
   * 2. Ensure gateway is running
   * 3. Resolve project + clone path
   * 4. Preview mode: fetch + worktree; Dev mode: use provided worktreePath
   * 5. Discover config, generate compose (no Caddy sidecar)
   * 6. composeUp, update gateway routes, health check
   * 7. Emit events and return status
   */
  async start(options: StartPreviewOptions): Promise<PreviewStatus> {
    const mode = options.mode ?? 'preview';

    // 1. Check Docker
    if (!(await isDockerAvailable())) {
      throw new Error(
        'Docker is not available. Please ensure Docker is installed and running.',
      );
    }

    // 2. Ensure gateway
    const gatewayPort = await this.gatewayManager.ensureGateway();

    // 3. Resolve project
    const project = await this.projectRepository.findById(options.projectId);
    if (!project) {
      throw new Error(`Project '${options.projectId}' not found`);
    }

    const clonePath = join(
      this.workspaceRoot,
      getProjectCloneDir(project.name, project.id),
    );

    // 4. Generate ID and prepare deploy dir
    const previewNanoid = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 10);
    const id = previewNanoid();
    const projectName = `${COMPOSE_PROJECT_PREFIX}${id}`;
    const deployDir = join(this.workspaceRoot, PREVIEWS_DIR, id);
    await mkdir(deployDir, { recursive: true });

    let sourcePath: string;
    let worktreeCreated = false;

    try {
      if (mode === 'preview') {
        // Fetch latest and create a worktree for the target branch
        try {
          const { simpleGit } = await import('simple-git');
          await simpleGit(clonePath).fetch();
        } catch (fetchErr) {
          log.warn({ err: fetchErr }, 'git fetch failed (may be offline)');
        }

        const worktreePath = join(deployDir, 'source');
        await createPreviewWorktree(clonePath, options.branch, worktreePath);
        worktreeCreated = true;
        sourcePath = worktreePath;
      } else {
        // Dev mode: use the provided worktree path
        if (!options.worktreePath) {
          throw new Error('worktreePath is required for dev mode');
        }
        sourcePath = options.worktreePath;
      }

      // 5. Discover config from source
      const config = await discoverConfig(sourcePath);

      // 6. Generate compose artifacts
      const labels = generateLabels({
        initiativeId: options.initiativeId,
        phaseId: options.phaseId,
        projectId: options.projectId,
        branch: options.branch,
        gatewayPort,
        previewId: id,
        mode,
        agentId: options.agentId,
      });

      const composeYaml = generateComposeFile(config, {
        projectPath: sourcePath,
        deploymentId: id,
        labels,
        mode,
      });

      const composePath = join(deployDir, 'docker-compose.yml');
      await writeFile(composePath, composeYaml, 'utf-8');

      log.info({ id, projectName, gatewayPort, composePath, mode }, 'preview deployment prepared');

      // Emit building event
      this.eventBus.emit<PreviewBuildingEvent>({
        type: 'preview:building',
        timestamp: new Date(),
        payload: {
          previewId: id,
          initiativeId: options.initiativeId,
          branch: options.branch,
          gatewayPort,
          mode,
          phaseId: options.phaseId,
        },
      });

      // 7. Build and start
      await composeUp(composePath, projectName);

      // 8. Build gateway routes and update Caddyfile
      const routes = this.buildRoutes(id, config);
      this.activeRoutes.set(id, routes);
      await this.gatewayManager.updateRoutes(this.activeRoutes);

      // 9. Health check
      const healthResults = await waitForHealthy(id, gatewayPort, config);
      const allHealthy = healthResults.every((r) => r.healthy);

      if (!allHealthy && healthResults.length > 0) {
        const failedServices = healthResults
          .filter((r) => !r.healthy)
          .map((r) => r.name);
        log.warn({ id, failedServices }, 'some services failed health checks');

        this.eventBus.emit<PreviewFailedEvent>({
          type: 'preview:failed',
          timestamp: new Date(),
          payload: {
            previewId: id,
            initiativeId: options.initiativeId,
            error: `Health checks failed for: ${failedServices.join(', ')}`,
          },
        });

        // Clean up
        await composeDown(projectName).catch(() => {});
        this.activeRoutes.delete(id);
        await this.gatewayManager.updateRoutes(this.activeRoutes);
        if (worktreeCreated) {
          await removePreviewWorktree(clonePath, join(deployDir, 'source')).catch(() => {});
        }
        await rm(deployDir, { recursive: true, force: true }).catch(() => {});

        // Stop gateway if no more previews
        if (this.activeRoutes.size === 0) {
          await this.gatewayManager.stopGateway().catch(() => {});
        }

        throw new Error(
          `Preview health checks failed for services: ${failedServices.join(', ')}`,
        );
      }

      // 10. Run seed commands
      await this.runSeeds(projectName, config);

      // 11. Success
      const url = `http://${id}.localhost:${gatewayPort}/`;
      log.info({ id, url }, 'preview deployment ready');

      this.eventBus.emit<PreviewReadyEvent>({
        type: 'preview:ready',
        timestamp: new Date(),
        payload: {
          previewId: id,
          initiativeId: options.initiativeId,
          branch: options.branch,
          gatewayPort,
          url,
          mode,
          phaseId: options.phaseId,
        },
      });

      const services = await composePs(projectName);

      return {
        id,
        projectName,
        initiativeId: options.initiativeId,
        phaseId: options.phaseId,
        projectId: options.projectId,
        branch: options.branch,
        gatewayPort,
        url,
        mode,
        status: 'running',
        services,
        composePath,
      };
    } catch (error) {
      // Clean up on any failure
      if (worktreeCreated) {
        await removePreviewWorktree(clonePath, join(deployDir, 'source')).catch(() => {});
      }

      // Only emit failed if we haven't already (health check path emits its own)
      const isHealthCheckError = (error as Error).message?.includes('health checks failed');
      if (!isHealthCheckError) {
        this.eventBus.emit<PreviewFailedEvent>({
          type: 'preview:failed',
          timestamp: new Date(),
          payload: {
            previewId: id,
            initiativeId: options.initiativeId,
            error: (error as Error).message,
          },
        });

        await composeDown(projectName).catch(() => {});
        this.activeRoutes.delete(id);
        await this.gatewayManager.updateRoutes(this.activeRoutes).catch(() => {});
        await rm(deployDir, { recursive: true, force: true }).catch(() => {});

        if (this.activeRoutes.size === 0) {
          await this.gatewayManager.stopGateway().catch(() => {});
        }
      }

      throw new Error(`Preview build failed: ${(error as Error).message}`);
    }
  }

  /**
   * Stop a preview deployment and clean up artifacts.
   */
  async stop(previewId: string): Promise<void> {
    const projectName = `${COMPOSE_PROJECT_PREFIX}${previewId}`;

    // Get labels before stopping to emit event and check mode
    const labels = await getContainerLabels(projectName);
    const initiativeId = labels[PREVIEW_LABELS.initiativeId] ?? '';
    const mode = labels[PREVIEW_LABELS.mode] as 'preview' | 'dev' | undefined;

    await composeDown(projectName);

    // Remove worktree if preview mode
    const deployDir = join(this.workspaceRoot, PREVIEWS_DIR, previewId);
    if (mode === 'preview' || mode === undefined) {
      const projectId = labels[PREVIEW_LABELS.projectId];
      if (projectId) {
        const project = await this.projectRepository.findById(projectId);
        if (project) {
          const clonePath = join(
            this.workspaceRoot,
            getProjectCloneDir(project.name, project.id),
          );
          await removePreviewWorktree(clonePath, join(deployDir, 'source')).catch(() => {});
        }
      }
    }

    // Clean up deployment directory
    await rm(deployDir, { recursive: true, force: true }).catch(() => {});

    // Update gateway routes
    this.activeRoutes.delete(previewId);
    await this.gatewayManager.updateRoutes(this.activeRoutes).catch(() => {});

    // Stop gateway if no more active previews
    if (this.activeRoutes.size === 0) {
      await this.gatewayManager.stopGateway().catch(() => {});
    }

    log.info({ previewId, projectName }, 'preview stopped');

    this.eventBus.emit<PreviewStoppedEvent>({
      type: 'preview:stopped',
      timestamp: new Date(),
      payload: { previewId, initiativeId },
    });
  }

  /**
   * List all active preview deployments, optionally filtered by initiative.
   */
  async list(initiativeId?: string): Promise<PreviewStatus[]> {
    const projects = await listPreviewProjects();
    const previews: PreviewStatus[] = [];

    for (const project of projects) {
      // Skip the gateway project
      if (project.Name === 'cw-preview-gateway') continue;

      const labels = await getContainerLabels(project.Name);
      if (!labels[PREVIEW_LABELS.preview]) continue;

      const preview = this.labelsToStatus(project.Name, labels, project.ConfigFiles);
      if (!preview) continue;

      if (initiativeId && preview.initiativeId !== initiativeId) continue;

      // Get service statuses
      preview.services = await composePs(project.Name);
      previews.push(preview);
    }

    return previews;
  }

  /**
   * Get the status of a specific preview deployment.
   */
  async getStatus(previewId: string): Promise<PreviewStatus | null> {
    const projectName = `${COMPOSE_PROJECT_PREFIX}${previewId}`;
    const labels = await getContainerLabels(projectName);

    if (!labels[PREVIEW_LABELS.preview]) {
      return null;
    }

    const preview = this.labelsToStatus(projectName, labels, '');
    if (!preview) return null;

    preview.services = await composePs(projectName);

    // Determine status from service states
    if (preview.services.length === 0) {
      preview.status = 'stopped';
    } else if (preview.services.every((s) => s.state === 'running')) {
      preview.status = 'running';
    } else if (preview.services.some((s) => s.state === 'exited' || s.state === 'dead')) {
      preview.status = 'failed';
    } else {
      preview.status = 'building';
    }

    return preview;
  }

  /**
   * Stop all preview deployments, then stop the gateway.
   */
  async stopAll(): Promise<void> {
    const projects = await listPreviewProjects();
    const previewProjects = projects.filter((p) => p.Name !== 'cw-preview-gateway');
    log.info({ count: previewProjects.length }, 'stopping all preview deployments');

    await Promise.all(
      previewProjects.map(async (project) => {
        const id = project.Name.replace(COMPOSE_PROJECT_PREFIX, '');
        await this.stop(id).catch((err) => {
          log.warn({ projectName: project.Name, err }, 'failed to stop preview');
        });
      }),
    );

    // Ensure gateway is stopped
    await this.gatewayManager.stopGateway().catch(() => {});
  }

  /**
   * Start a preview for an agent, resolving initiative/project/branch automatically.
   * The agent's worktree determines the source path and branch.
   */
  async startForAgent(agentId: string, agentManager: AgentManager): Promise<PreviewStatus> {
    const agent = await agentManager.get(agentId);
    if (!agent) throw new Error(`Agent '${agentId}' not found`);
    if (!agent.initiativeId) throw new Error('Agent has no initiative');

    const projects = await this.projectRepository.findProjectsByInitiativeId(agent.initiativeId);
    if (projects.length !== 1) {
      throw new Error(`Expected 1 project for initiative, found ${projects.length}`);
    }
    const project = projects[0];

    const agentWorkdir = join(this.workspaceRoot, 'agent-workdirs', agent.worktreeId);
    const worktreePath = join(agentWorkdir, project.name);

    const { simpleGit } = await import('simple-git');
    const git = simpleGit(worktreePath);
    const { current: branch } = await git.branchLocal();

    return this.start({
      initiativeId: agent.initiativeId,
      projectId: project.id,
      branch: branch || 'main',
      mode: 'dev',
      worktreePath,
      agentId,
    });
  }

  /**
   * Stop all previews associated with a specific agent ID (best-effort).
   */
  async stopByAgentId(agentId: string): Promise<void> {
    try {
      const projects = await listPreviewProjects();
      for (const project of projects) {
        if (project.Name === 'cw-preview-gateway') continue;
        const labels = await getContainerLabels(project.Name);
        if (labels[PREVIEW_LABELS.agentId] === agentId) {
          const previewId = labels[PREVIEW_LABELS.previewId] ?? project.Name.replace(COMPOSE_PROJECT_PREFIX, '');
          log.info({ previewId, agentId }, 'stopping preview for agent');
          await this.stop(previewId).catch((err) => {
            log.warn({ previewId, agentId, err }, 'failed to stop preview for agent');
          });
        }
      }
    } catch (err) {
      log.warn({ agentId, err }, 'stopByAgentId failed (best-effort)');
    }
  }

  /**
   * Register event listener for auto-starting previews on phase:pending_review.
   */
  private setupEventListeners(): void {
    this.eventBus.on<PhasePendingReviewEvent>(
      'phase:pending_review',
      async (event) => {
        try {
          const { phaseId, initiativeId } = event.payload;

          const initiative = await this.initiativeRepository.findById(initiativeId);
          if (!initiative?.branch) {
            log.debug({ initiativeId }, 'no initiative branch, skipping auto-preview');
            return;
          }

          const phase = await this.phaseRepository.findById(phaseId);
          if (!phase) {
            log.debug({ phaseId }, 'phase not found, skipping auto-preview');
            return;
          }

          const projects = await this.projectRepository.findProjectsByInitiativeId(initiativeId);
          if (projects.length !== 1) {
            log.debug(
              { initiativeId, projectCount: projects.length },
              'auto-preview requires exactly one project',
            );
            return;
          }

          const branch = phaseBranchName(initiative.branch, phase.name);

          log.info(
            { initiativeId, phaseId, branch, projectId: projects[0].id },
            'auto-starting preview for pending_review phase',
          );

          await this.start({
            initiativeId,
            phaseId,
            projectId: projects[0].id,
            branch,
            mode: 'preview',
          });
        } catch (error) {
          log.warn({ err: error }, 'auto-preview failed (best-effort)');
        }
      },
    );

    // Auto-teardown previews when their owning agent stops
    this.eventBus.on<AgentStoppedEvent>('agent:stopped', async (event) => {
      await this.stopByAgentId(event.payload.agentId);
    });
  }

  /**
   * Run seed commands for each service that has them configured.
   * Executes after health checks pass, before the preview is marked ready.
   */
  private async runSeeds(projectName: string, config: PreviewConfig): Promise<void> {
    for (const [serviceName, svc] of Object.entries(config.services)) {
      if (!svc.seed?.length) continue;
      log.info({ projectName, service: serviceName, count: svc.seed.length }, 'running seed commands');
      for (const cmd of svc.seed) {
        log.info({ service: serviceName, cmd }, 'executing seed');
        await execInContainer(projectName, serviceName, cmd);
      }
    }
  }

  /**
   * Build gateway routes from a preview config.
   */
  private buildRoutes(previewId: string, config: PreviewConfig): GatewayRoute[] {
    const routes: GatewayRoute[] = [];

    for (const [name, svc] of Object.entries(config.services)) {
      if (svc.internal) continue;

      routes.push({
        containerName: `cw-preview-${previewId}-${name}`,
        port: svc.port,
        route: svc.route ?? '/',
      });
    }

    return routes;
  }

  /**
   * Reconstruct PreviewStatus from Docker container labels.
   */
  private labelsToStatus(
    projectName: string,
    labels: Record<string, string>,
    composePath: string,
  ): PreviewStatus | null {
    const previewId = labels[PREVIEW_LABELS.previewId] ?? projectName.replace(COMPOSE_PROJECT_PREFIX, '');
    const initiativeId = labels[PREVIEW_LABELS.initiativeId];
    const projectId = labels[PREVIEW_LABELS.projectId];
    const branch = labels[PREVIEW_LABELS.branch];
    const gatewayPort = parseInt(labels[PREVIEW_LABELS.port] ?? '0', 10);
    const mode = (labels[PREVIEW_LABELS.mode] as 'preview' | 'dev') ?? 'preview';

    if (!initiativeId || !projectId || !branch) {
      return null;
    }

    return {
      id: previewId,
      projectName,
      initiativeId,
      phaseId: labels[PREVIEW_LABELS.phaseId],
      projectId,
      branch,
      gatewayPort,
      url: `http://${previewId}.localhost:${gatewayPort}/`,
      mode,
      status: 'running',
      services: [],
      composePath,
    };
  }
}
