/**
 * Preview Manager
 *
 * Orchestrates preview deployment lifecycle: start, stop, list, status.
 * Uses Docker as the source of truth — no database persistence.
 */

import { join } from 'node:path';
import { mkdir, writeFile, rm } from 'node:fs/promises';
import { nanoid } from 'nanoid';
import type { ProjectRepository } from '../db/repositories/project-repository.js';
import type { EventBus } from '../events/types.js';
import type {
  PreviewStatus,
  StartPreviewOptions,
} from './types.js';
import { COMPOSE_PROJECT_PREFIX, PREVIEW_LABELS } from './types.js';
import { discoverConfig } from './config-reader.js';
import { generateComposeFile, generateCaddyfile, generateLabels } from './compose-generator.js';
import {
  isDockerAvailable,
  composeUp,
  composeDown,
  composePs,
  listPreviewProjects,
  getContainerLabels,
} from './docker-client.js';
import { waitForHealthy } from './health-checker.js';
import { allocatePort } from './port-allocator.js';
import { getProjectCloneDir } from '../git/project-clones.js';
import { createModuleLogger } from '../logger/index.js';
import type {
  PreviewBuildingEvent,
  PreviewReadyEvent,
  PreviewStoppedEvent,
  PreviewFailedEvent,
} from '../events/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;

  constructor(
    projectRepository: ProjectRepository,
    eventBus: EventBus,
    workspaceRoot: string,
  ) {
    this.projectRepository = projectRepository;
    this.eventBus = eventBus;
    this.workspaceRoot = workspaceRoot;
  }

  /**
   * Start a preview deployment.
   *
   * 1. Check Docker availability
   * 2. Resolve project clone path
   * 3. Discover config from project at target branch
   * 4. Allocate port, generate ID
   * 5. Generate compose + Caddyfile, write to .cw-previews/<id>/
   * 6. Run composeUp, wait for healthy
   * 7. Emit events and return status
   */
  async start(options: StartPreviewOptions): Promise<PreviewStatus> {
    // 1. Check Docker
    if (!(await isDockerAvailable())) {
      throw new Error(
        'Docker is not available. Please ensure Docker is installed and running.',
      );
    }

    // 2. 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),
    );

    // 3. Discover config
    const config = await discoverConfig(clonePath);

    // 4. Allocate port and generate ID
    const port = await allocatePort();
    const id = nanoid(10);
    const projectName = `${COMPOSE_PROJECT_PREFIX}${id}`;

    // 5. Generate compose artifacts
    const labels = generateLabels({
      initiativeId: options.initiativeId,
      phaseId: options.phaseId,
      projectId: options.projectId,
      branch: options.branch,
      port,
      previewId: id,
    });

    const composeYaml = generateComposeFile(config, {
      projectPath: clonePath,
      port,
      deploymentId: id,
      labels,
    });
    const caddyfile = generateCaddyfile(config);

    // Write artifacts
    const deployDir = join(this.workspaceRoot, PREVIEWS_DIR, id);
    await mkdir(deployDir, { recursive: true });

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

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

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

    // 7. Build and start
    try {
      await composeUp(composePath, projectName);
    } catch (error) {
      log.error({ id, err: error }, 'compose up failed');

      this.eventBus.emit<PreviewFailedEvent>({
        type: 'preview:failed',
        timestamp: new Date(),
        payload: {
          previewId: id,
          initiativeId: options.initiativeId,
          error: (error as Error).message,
        },
      });

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

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

    // 8. Health check
    const healthResults = await waitForHealthy(port, 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(', ')}`,
        },
      });

      await composeDown(projectName).catch(() => {});
      await rm(deployDir, { recursive: true, force: true }).catch(() => {});

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

    // 9. Success
    const url = `http://localhost:${port}`;
    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,
        port,
        url,
      },
    });

    const services = await composePs(projectName);

    return {
      id,
      projectName,
      initiativeId: options.initiativeId,
      phaseId: options.phaseId,
      projectId: options.projectId,
      branch: options.branch,
      port,
      status: 'running',
      services,
      composePath,
    };
  }

  /**
   * 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
    const labels = await getContainerLabels(projectName);
    const initiativeId = labels[PREVIEW_LABELS.initiativeId] ?? '';

    await composeDown(projectName);

    // Clean up deployment directory
    const deployDir = join(this.workspaceRoot, PREVIEWS_DIR, previewId);
    await rm(deployDir, { recursive: true, force: true }).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) {
      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. Called on server shutdown.
   */
  async stopAll(): Promise<void> {
    const projects = await listPreviewProjects();
    log.info({ count: projects.length }, 'stopping all preview deployments');

    await Promise.all(
      projects.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');
        });
      }),
    );
  }

  /**
   * 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 port = parseInt(labels[PREVIEW_LABELS.port] ?? '0', 10);

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

    return {
      id: previewId,
      projectName,
      initiativeId,
      phaseId: labels[PREVIEW_LABELS.phaseId],
      projectId,
      branch,
      port,
      status: 'running',
      services: [],
      composePath,
    };
  }
}
