import fs from 'fs/promises';
import path from 'path';
import { exec as execCb } from 'child_process';
import { promisify } from 'util';
import { parse as parseDotenv } from 'dotenv';
import { prisma } from '../lib/prisma';
import { env } from '../config/env';
import { logger } from '../utils/logger';
import { registerInstance } from '../modules/instances/instances.service';
import * as docker from './docker.service';

const exec = promisify(execCb);

// ─── Types ──────────────────────────────────────────────────────────

export interface DiscoveredInstance {
  name: string;
  slug: string;
  domain: string;
  basePath: string;
  composeProject: string;
  portConfig: { api: number; admin: number; postgres: number; nginx: number; embed: number };
  adminEmail: string;
  enableMedia: boolean;
  enableChat: boolean;
  enableGancio: boolean;
  enableListmonk: boolean;
  enableMonitoring: boolean;
  enableDevTools: boolean;
  enablePayments: boolean;
  enableMeet: boolean;
  enableSms: boolean;
  enableSocial: boolean;
  enablePeople: boolean;
  enableAnalytics: boolean;
  emailTestMode: boolean;
  // Discovery metadata (UI-only, not persisted)
  source: 'parent' | 'docker';
  isRunning: boolean;
  runningContainers: number;
  totalContainers: number;
  isAlreadyRegistered: boolean;
  existingInstanceId?: string;
  isParentInstance: boolean;
}

export interface DiscoverySummary {
  total: number;
  newInstances: number;
  alreadyRegistered: number;
  running: number;
  parentFound: boolean;
}

export interface DiscoveryResult {
  instances: DiscoveredInstance[];
  summary: DiscoverySummary;
}

interface ComposeProject {
  Name: string;
  Status: string;
  ConfigFiles: string;
}

// ─── .env Parser ────────────────────────────────────────────────────

/**
 * Parse a CML instance's .env file and extract safe configuration metadata.
 * Never reads secrets (JWT keys, passwords, encryption keys).
 */
async function parseCmlEnv(envPath: string): Promise<Record<string, string> | null> {
  try {
    const content = await fs.readFile(envPath, 'utf-8');
    return parseDotenv(Buffer.from(content));
  } catch {
    return null;
  }
}

function extractPortConfig(envVars: Record<string, string>): DiscoveredInstance['portConfig'] {
  const nginx = parseInt(envVars.NGINX_HTTP_PORT || '80', 10);
  return {
    api: parseInt(envVars.API_PORT || '4000', 10),
    admin: parseInt(envVars.ADMIN_PORT || '3000', 10),
    postgres: parseInt(envVars.V2_POSTGRES_PORT || '5433', 10),
    nginx,
    embed: parseInt(envVars.NOCODB_EMBED_PORT || String(nginx + 1), 10),
  };
}

function extractFeatureFlags(envVars: Record<string, string>) {
  const isTrue = (val?: string) => val?.toLowerCase() === 'true';
  return {
    enableMedia: isTrue(envVars.ENABLE_MEDIA_FEATURES),
    enableChat: isTrue(envVars.ENABLE_CHAT),
    enableGancio: isTrue(envVars.GANCIO_SYNC_ENABLED),
    enableListmonk: isTrue(envVars.LISTMONK_SYNC_ENABLED),
    enablePayments: isTrue(envVars.ENABLE_PAYMENTS),
    enableMeet: isTrue(envVars.ENABLE_MEET),
    enableSms: isTrue(envVars.ENABLE_SMS),
    enableSocial: isTrue(envVars.ENABLE_SOCIAL),
    enablePeople: isTrue(envVars.ENABLE_PEOPLE),
    enableAnalytics: isTrue(envVars.ENABLE_ANALYTICS),
    emailTestMode: isTrue(envVars.EMAIL_TEST_MODE),
  };
}

function slugify(str: string): string {
  return str
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-|-$/g, '')
    .substring(0, 50);
}

// ─── CML Fingerprint Test ───────────────────────────────────────────

/**
 * Check if a directory is a CML instance by verifying key files exist
 * and the .env has CML-specific variables.
 */
async function isCmlInstance(dirPath: string): Promise<boolean> {
  try {
    // Must have docker-compose.yml and api/prisma/schema.prisma
    await fs.access(path.join(dirPath, 'docker-compose.yml'));
    await fs.access(path.join(dirPath, 'api', 'prisma', 'schema.prisma'));

    // Must have .env with DOMAIN and JWT_ACCESS_SECRET
    const envVars = await parseCmlEnv(path.join(dirPath, '.env'));
    if (!envVars) return false;
    return !!(envVars.DOMAIN && envVars.JWT_ACCESS_SECRET);
  } catch {
    return false;
  }
}

// ─── Container Status ───────────────────────────────────────────────

async function getContainerCounts(
  projectDir: string,
  composeProject: string
): Promise<{ running: number; total: number }> {
  try {
    // Use docker.composePs which validates the project name
    const containers = await docker.composePs(projectDir, composeProject);
    const running = containers.filter((c) => c.state === 'running').length;
    return { running, total: containers.length };
  } catch {
    return { running: 0, total: 0 };
  }
}

// ─── Compose Project Discovery ──────────────────────────────────────

/**
 * List all Docker Compose projects on the host via `docker compose ls`.
 */
async function listComposeProjects(): Promise<ComposeProject[]> {
  try {
    const { stdout } = await exec('docker compose ls --format json', {
      timeout: 15_000,
      env: { ...process.env, COMPOSE_ANSI: 'never' },
    });
    if (!stdout.trim()) return [];
    return JSON.parse(stdout);
  } catch (err) {
    logger.warn(`[discovery] Failed to list compose projects: ${(err as Error).message}`);
    return [];
  }
}

/**
 * Derive the compose project name from a directory path.
 * Docker Compose defaults to the directory basename (lowercased, special chars replaced).
 */
function deriveComposeProject(dirPath: string, projects: ComposeProject[]): string {
  // Try to find project by matching config file path
  const composePath = path.join(dirPath, 'docker-compose.yml');
  for (const p of projects) {
    // ConfigFiles can be comma-separated
    const configs = p.ConfigFiles.split(',').map((s) => s.trim());
    if (configs.some((c) => path.resolve(c) === path.resolve(composePath))) {
      return p.Name;
    }
  }
  // Fall back to directory basename convention
  return path.basename(dirPath).toLowerCase().replace(/[^a-z0-9]/g, '');
}

// ─── Build Discovered Instance ──────────────────────────────────────

async function buildDiscoveredInstance(
  dirPath: string,
  source: 'parent' | 'docker',
  composeProject: string,
  existingInstances: Array<{ id: string; basePath: string; composeProject: string; domain: string }>
): Promise<DiscoveredInstance | null> {
  const envVars = await parseCmlEnv(path.join(dirPath, '.env'));
  if (!envVars) return null;

  const domain = envVars.DOMAIN || path.basename(dirPath);
  const portConfig = extractPortConfig(envVars);
  const flags = extractFeatureFlags(envVars);

  // Check container status
  const { running, total } = await getContainerCounts(dirPath, composeProject);

  // Infer monitoring/devtools from running containers (these aren't .env flags)
  let enableMonitoring = false;
  let enableDevTools = false;
  try {
    // Use docker.composePs which validates the project name (prevents shell injection)
    const containers = await docker.composePs(dirPath, composeProject);
    const serviceNames = containers.map((c) => c.service.toLowerCase());
    enableMonitoring = serviceNames.some((n) => n.includes('prometheus') || n.includes('grafana'));
    enableDevTools = serviceNames.some((n) => n.includes('code-server') || n.includes('gitea') || n.includes('n8n'));
  } catch {
    // Couldn't inspect services — leave as false
  }

  // Check deduplication against existing instances
  const resolvedPath = path.resolve(dirPath);
  const match = existingInstances.find(
    (inst) =>
      path.resolve(inst.basePath) === resolvedPath ||
      inst.composeProject === composeProject ||
      inst.domain === domain
  );

  const isParent = source === 'parent' ||
    (!!env.CML_SOURCE_PATH && path.resolve(env.CML_SOURCE_PATH) === resolvedPath);

  return {
    name: envVars.SITE_NAME || domain.split('.')[0] || path.basename(dirPath),
    slug: slugify(envVars.SITE_NAME || domain.split('.')[0] || path.basename(dirPath)),
    domain,
    basePath: resolvedPath,
    composeProject,
    portConfig,
    adminEmail: envVars.INITIAL_ADMIN_EMAIL || 'admin@localhost',
    ...flags,
    enableMonitoring,
    enableDevTools,
    source,
    isRunning: running > 0,
    runningContainers: running,
    totalContainers: total,
    isAlreadyRegistered: !!match,
    existingInstanceId: match?.id,
    isParentInstance: isParent,
  };
}

// ─── Main Discovery Function ────────────────────────────────────────

export async function discoverInstances(): Promise<DiscoveryResult> {
  const discovered: DiscoveredInstance[] = [];
  const seenPaths = new Set<string>();

  // Load existing instances for deduplication
  const existingInstances = await prisma.instance.findMany({
    select: { id: true, basePath: true, composeProject: true, domain: true },
  });

  // List all compose projects upfront
  const composeProjects = await listComposeProjects();

  // Strategy 1: Parent instance (from CML_SOURCE_PATH)
  if (env.CML_SOURCE_PATH) {
    const parentPath = path.resolve(env.CML_SOURCE_PATH);
    if (await isCmlInstance(parentPath)) {
      const project = deriveComposeProject(parentPath, composeProjects);
      const inst = await buildDiscoveredInstance(parentPath, 'parent', project, existingInstances);
      if (inst) {
        // Ensure parent gets a sensible name
        if (inst.isParentInstance && inst.name === path.basename(parentPath)) {
          inst.name = `Parent (${inst.domain})`;
        }
        discovered.push(inst);
        seenPaths.add(parentPath);
      }
    } else {
      logger.debug(`[discovery] CML_SOURCE_PATH (${env.CML_SOURCE_PATH}) is not a valid CML instance`);
    }
  }

  // Strategy 2: Docker scan — check all running compose projects
  for (const project of composeProjects) {
    // Skip CCP's own project
    if (project.Name.startsWith('ccp-') || project.Name.startsWith('changemaker-control-panel')) {
      continue;
    }

    // Extract project directory from config file path
    if (!project.ConfigFiles) continue;
    const configFile = project.ConfigFiles.split(',')[0].trim();
    const projectDir = path.dirname(configFile);
    const resolvedDir = path.resolve(projectDir);

    // Skip if already found via parent strategy
    if (seenPaths.has(resolvedDir)) continue;

    // CML fingerprint test
    if (!(await isCmlInstance(resolvedDir))) continue;

    const inst = await buildDiscoveredInstance(resolvedDir, 'docker', project.Name, existingInstances);
    if (inst) {
      discovered.push(inst);
      seenPaths.add(resolvedDir);
    }
  }

  // Build summary
  const newInstances = discovered.filter((d) => !d.isAlreadyRegistered).length;
  const alreadyRegistered = discovered.filter((d) => d.isAlreadyRegistered).length;
  const running = discovered.filter((d) => d.isRunning).length;
  const parentFound = discovered.some((d) => d.isParentInstance);

  return {
    instances: discovered,
    summary: {
      total: discovered.length,
      newInstances,
      alreadyRegistered,
      running,
      parentFound,
    },
  };
}

// ─── Auto-Import on First Boot ──────────────────────────────────────

/**
 * Checks if the CCP database has zero instances. If empty, discovers
 * the parent instance and auto-registers it.
 * Called 5s after server startup via setTimeout.
 */
export async function autoDiscoverOnStartup(): Promise<void> {
  const count = await prisma.instance.count();
  if (count > 0) {
    logger.debug('[discovery] Instances already exist, skipping auto-discovery');
    return;
  }

  logger.info('[discovery] No instances found — running auto-discovery...');

  const result = await discoverInstances();
  if (result.instances.length === 0) {
    logger.info('[discovery] No CML instances discovered on this host');
    return;
  }

  // Auto-register parent instance first, then any others
  const sorted = [...result.instances].sort((a, b) => {
    if (a.isParentInstance && !b.isParentInstance) return -1;
    if (!a.isParentInstance && b.isParentInstance) return 1;
    return 0;
  });

  // Get or create a system user ID for audit logging
  const systemUser = await prisma.ccpUser.findFirst({
    where: { role: 'SUPER_ADMIN' },
    select: { id: true },
  });
  const userId = systemUser?.id || 'system';

  let imported = 0;
  for (const inst of sorted) {
    if (inst.isAlreadyRegistered) continue;
    try {
      await registerInstance(
        {
          name: inst.name,
          slug: inst.slug,
          domain: inst.domain,
          basePath: inst.basePath,
          composeProject: inst.composeProject,
          portConfig: inst.portConfig,
          adminEmail: inst.adminEmail,
          enableMedia: inst.enableMedia,
          enableChat: inst.enableChat,
          enableGancio: inst.enableGancio,
          enableListmonk: inst.enableListmonk,
          enableMonitoring: inst.enableMonitoring,
          enableDevTools: inst.enableDevTools,
          enablePayments: inst.enablePayments,
          enableMeet: inst.enableMeet,
          enableSms: inst.enableSms,
          enableSocial: inst.enableSocial,
          enablePeople: inst.enablePeople,
          enableAnalytics: inst.enableAnalytics,
          emailTestMode: inst.emailTestMode,
        },
        userId,
        'auto-discovery'
      );
      imported++;
      logger.info(`[discovery] Auto-registered instance: ${inst.name} (${inst.domain})`);
    } catch (err) {
      logger.warn(`[discovery] Failed to auto-register ${inst.name}: ${(err as Error).message}`);
    }
  }

  logger.info(`[discovery] Auto-discovery complete: ${imported}/${sorted.filter((s) => !s.isAlreadyRegistered).length} instances registered`);
}
