import { randomBytes } from 'node:crypto';
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'node:fs';
import { join } from 'node:path';
import { homedir } from 'node:os';
import { DependencyChecker, type DependencyStatus } from './deps.js';
import { DependencyInstaller, type InstallProgress } from './installer.js';

export { DependencyChecker, type DependencyStatus } from './deps.js';
export { DependencyInstaller, type InstallProgress } from './installer.js';
export { ServiceManager, type ServiceStatus } from './service.js';

export interface SetupConfig {
  masterKey: string;
  /**
   * Shared secret the inbound-email webhook authenticates against
   * (`X-Inbound-Secret` header). Auto-minted at setup time and
   * persisted alongside `masterKey` so every API restart reuses the
   * same value — without this, the API generated a fresh secret on
   * every cold start and printed a noisy warning to the operator.
   *
   * Optional in the type for backward compatibility with existing
   * on-disk configs; {@link SetupManager.initConfig} lazy-mints it
   * into older configs the first time it loads them.
   */
  inboundSecret?: string;
  stalwart: {
    url: string;
    adminUser: string;
    adminPassword: string;
  };
  smtp: { host: string; port: number };
  imap: { host: string; port: number };
  api: { port: number; host: string };
  dataDir: string;
}

export interface SetupResult {
  configPath: string;
  envPath: string;
  config: SetupConfig;
  isNew: boolean;
}

/**
 * SetupManager orchestrates AgenticMail initialization:
 * config generation, dependency installation, and service startup.
 * All dependencies are required and auto-installed.
 */
export class SetupManager {
  private checker = new DependencyChecker();
  private installer: DependencyInstaller;

  constructor(onProgress?: InstallProgress) {
    this.installer = new DependencyInstaller(onProgress);
  }

  /**
   * Check all dependencies and return their status.
   */
  async checkDependencies(): Promise<DependencyStatus[]> {
    return this.checker.checkAll();
  }

  /**
   * Install all missing dependencies automatically.
   * Docker → Stalwart → cloudflared. Nothing is optional.
   */
  async installAll(composePath: string): Promise<void> {
    await this.installer.installAll(composePath);
  }

  /**
   * Ensure a specific dependency is installed.
   */
  async ensureDocker(): Promise<void> {
    await this.installer.installDocker();
  }

  async ensureStalwart(composePath?: string): Promise<void> {
    const status = await this.checker.checkStalwart();
    if (status.installed) return;
    const path = composePath ?? this.getComposePath();
    await this.installer.startStalwart(path);
  }

  async ensureCloudflared(): Promise<string> {
    return this.installer.installCloudflared();
  }

  /**
   * Get the path to docker-compose.yml.
   * Prefers ~/.agenticmail/docker-compose.yml (standalone),
   * falls back to monorepo location.
   */
  getComposePath(): string {
    const standalonePath = join(homedir(), '.agenticmail', 'docker-compose.yml');
    if (existsSync(standalonePath)) return standalonePath;
    // Monorepo fallback: search common locations
    const cwd = process.cwd();
    const candidates = [cwd, join(cwd, '..')];
    for (const dir of candidates) {
      const p = join(dir, 'docker-compose.yml');
      if (existsSync(p)) return p;
    }
    return standalonePath; // Will be generated by initConfig
  }

  /**
   * Initialize AgenticMail config files.
   * If config already exists, returns the existing config without overwriting.
   * Always regenerates Docker files to keep passwords in sync.
   */
  initConfig(): SetupResult {
    const dataDir = join(homedir(), '.agenticmail');
    const configPath = join(dataDir, 'config.json');
    const envPath = join(dataDir, '.env');

    // If config already exists, load it and regenerate Docker files
    if (existsSync(configPath)) {
      try {
        const existing = JSON.parse(readFileSync(configPath, 'utf-8')) as SetupConfig;
        // v0.9.84 — lazy-mint inboundSecret into pre-existing configs
        // that predate this field. Without this, every API restart on
        // a brand-new-install machine churned out a fresh random
        // secret + "set this env var" warning. We mint once, persist
        // back, and the .env file gets the same value so subsequent
        // `agenticmail start` runs reuse it.
        let needsRewrite = false;
        if (!existing.inboundSecret) {
          existing.inboundSecret = `inb_${randomBytes(24).toString('hex')}`;
          needsRewrite = true;
        }
        if (needsRewrite) {
          writeFileSync(configPath, JSON.stringify(existing, null, 2));
          chmodSync(configPath, 0o600);
          // Append the inbound line to .env so `agenticmail start`
          // (which sources .env, not config.json) sees it on the next
          // boot. We append rather than rewrite to avoid clobbering
          // user-added lines.
          this.ensureEnvHasInboundSecret(envPath, existing.inboundSecret);
        }
        this.generateDockerFiles(existing);
        return { configPath, envPath, config: existing, isNew: false };
      } catch { /* fall through to create new */ }
    }

    // Create data directory
    if (!existsSync(dataDir)) {
      mkdirSync(dataDir, { recursive: true });
    }

    const masterKey = `mk_${randomBytes(24).toString('hex')}`;
    const stalwartPassword = randomBytes(16).toString('hex');
    const inboundSecret = `inb_${randomBytes(24).toString('hex')}`;

    const config: SetupConfig = {
      masterKey,
      inboundSecret,
      stalwart: {
        url: 'http://localhost:8080',
        adminUser: 'admin',
        adminPassword: stalwartPassword,
      },
      smtp: { host: 'localhost', port: 587 },
      imap: { host: 'localhost', port: 143 },
      api: { port: 3829, host: '127.0.0.1' },
      dataDir,
    };

    writeFileSync(configPath, JSON.stringify(config, null, 2));
    chmodSync(configPath, 0o600);

    // Create .env file
    const envContent = `# Auto-generated by agenticmail setup
STALWART_ADMIN_USER=admin
STALWART_ADMIN_PASSWORD=${stalwartPassword}
STALWART_URL=http://localhost:8080

AGENTICMAIL_MASTER_KEY=${masterKey}
AGENTICMAIL_INBOUND_SECRET=${inboundSecret}
AGENTICMAIL_API_PORT=3829
AGENTICMAIL_DATA_DIR=${dataDir}

SMTP_HOST=localhost
SMTP_PORT=587
IMAP_HOST=localhost
IMAP_PORT=143
`;

    writeFileSync(envPath, envContent);
    chmodSync(envPath, 0o600);

    // Generate Docker infrastructure files
    this.generateDockerFiles(config);

    return { configPath, envPath, config, isNew: true };
  }

  /**
   * Append `AGENTICMAIL_INBOUND_SECRET=...` to .env if the file does
   * not already contain that key. Used by the lazy-mint path to make
   * sure existing installs pick up the new secret on next boot
   * without clobbering anything the operator added.
   */
  private ensureEnvHasInboundSecret(envPath: string, secret: string): void {
    if (!existsSync(envPath)) return;  // no .env yet — config.json drives this install
    try {
      const current = readFileSync(envPath, 'utf-8');
      if (/^AGENTICMAIL_INBOUND_SECRET=/m.test(current)) return; // already set
      const updated = current + (current.endsWith('\n') ? '' : '\n')
        + `AGENTICMAIL_INBOUND_SECRET=${secret}\n`;
      writeFileSync(envPath, updated);
      chmodSync(envPath, 0o600);
    } catch { /* best effort */ }
  }

  /**
   * Generate docker-compose.yml and stalwart.toml in ~/.agenticmail/
   * with the correct admin password from config.
   */
  private generateDockerFiles(config: SetupConfig): void {
    const dataDir = config.dataDir || join(homedir(), '.agenticmail');
    if (!existsSync(dataDir)) {
      mkdirSync(dataDir, { recursive: true });
    }

    const password = config.stalwart?.adminPassword || 'changeme';

    const composePath = join(dataDir, 'docker-compose.yml');
    writeFileSync(composePath, `services:
  stalwart:
    # Pinned to v0.15.5 — Stalwart 0.16+ moved its config to JSON
    # at /etc/stalwart/config.json (hardcoded into the container
    # CMD), runs as UID 2000, and silently ignores our pre-0.10
    # TOML mount. On those builds Stalwart enters bootstrap mode
    # and the setup wizard 404s on the admin API. Pinning until
    # the templates are migrated to the 0.16+ JSON layout.
    # Tracking: https://github.com/agenticmail/agenticmail/issues/10
    image: stalwartlabs/stalwart:v0.15.5
    container_name: agenticmail-stalwart
    ports:
      - "127.0.0.1:8080:8080"   # HTTP Admin + JMAP (localhost only)
      - "127.0.0.1:587:587"     # SMTP Submission (localhost only)
      - "127.0.0.1:143:143"     # IMAP (localhost only)
      - "127.0.0.1:25:25"       # SMTP (localhost only)
    volumes:
      - stalwart-data:/opt/stalwart
      - ./stalwart.toml:/opt/stalwart/etc/config.toml:ro
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "curl -sf http://localhost:8080/health || true"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  stalwart-data:
`);
    chmodSync(composePath, 0o600);

    const tomlPath = join(dataDir, 'stalwart.toml');
    writeFileSync(tomlPath, `# Stalwart Mail Server — AgenticMail Configuration

[server]
hostname = "localhost"

[server.listener.smtp]
bind = ["0.0.0.0:25"]
protocol = "smtp"

[server.listener.submission]
bind = ["0.0.0.0:587"]
protocol = "smtp"
tls.implicit = false

[server.listener.imap]
bind = ["0.0.0.0:143"]
protocol = "imap"
tls.implicit = false

[server.listener.http]
bind = ["0.0.0.0:8080"]
protocol = "http"
tls.implicit = false

[storage]
data = "rocksdb"
fts = "rocksdb"
blob = "rocksdb"
lookup = "rocksdb"
directory = "internal"

[store.rocksdb]
type = "rocksdb"
path = "/opt/stalwart/data"
compression = "lz4"

[directory.internal]
type = "internal"
store = "rocksdb"

[tracer.stdout]
type = "stdout"
level = "info"
ansi = true
enable = true

[authentication.fallback-admin]
user = "admin"
secret = "${password}"
`);
    chmodSync(tomlPath, 0o600);
  }

  /**
   * Check if config has already been initialized.
   */
  isInitialized(): boolean {
    const configPath = join(homedir(), '.agenticmail', 'config.json');
    return existsSync(configPath);
  }
}
