import fs from 'fs/promises';
import path from 'path';
import { parse as parseDotenv } from 'dotenv';
import { InstanceStatus, HealthStatus } from '@prisma/client';
import { prisma } from '../lib/prisma';
import * as docker from './docker.service';
import { getDriverForInstance, AgentUnreachableError } from './execution-driver';
import { logger } from '../utils/logger';
import { createEvent } from './event.service';
import type { ContainerInfo } from './docker.service';

/**
 * Determine overall health status from container list.
 */
function determineHealth(containers: ContainerInfo[]): {
  status: HealthStatus;
  serviceStatus: Record<string, { state: string; health: string }>;
  totalServices: number;
  healthyServices: number;
} {
  if (containers.length === 0) {
    return { status: HealthStatus.UNKNOWN, serviceStatus: {}, totalServices: 0, healthyServices: 0 };
  }

  const serviceStatus: Record<string, { state: string; health: string }> = {};
  let healthyCount = 0;
  let runningCount = 0;

  for (const c of containers) {
    serviceStatus[c.service || c.name] = {
      state: c.state,
      health: c.health,
    };

    const isRunning = c.state === 'running';
    if (isRunning) runningCount++;

    // A service is "healthy" if it's running AND either has no health check or passes it
    const isHealthy = isRunning && (c.health === '' || c.health === 'healthy');
    if (isHealthy) healthyCount++;
  }

  const total = containers.length;
  let status: HealthStatus;

  if (healthyCount === total) {
    status = HealthStatus.HEALTHY;
  } else if (runningCount === 0) {
    status = HealthStatus.UNHEALTHY;
  } else if (healthyCount >= total / 2) {
    status = HealthStatus.DEGRADED;
  } else {
    status = HealthStatus.UNHEALTHY;
  }

  return { status, serviceStatus, totalServices: total, healthyServices: healthyCount };
}

/**
 * Parse an instance's .env file and return all variables.
 * Returns null if the file doesn't exist or can't be read.
 */
async function readEnvFile(basePath: string): Promise<Record<string, string> | null> {
  try {
    const content = await fs.readFile(path.join(basePath, '.env'), 'utf-8');
    return parseDotenv(Buffer.from(content));
  } catch {
    return null;
  }
}

/**
 * Extract feature flags from parsed .env variables.
 */
function extractFeatureFlags(envVars: Record<string, string>): Record<string, boolean> {
  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),
  };
}

/**
 * Check the health of a single instance. Returns the created HealthCheck record.
 * Also auto-corrects instance.status based on actual container state:
 *   - RUNNING instance with 0 containers → STOPPED
 *   - STOPPED instance with running containers → RUNNING
 */
export async function checkInstanceHealth(instanceId: string) {
  const instance = await prisma.instance.findUnique({ where: { id: instanceId } });
  if (!instance) {
    throw new Error(`Instance ${instanceId} not found`);
  }

  // Only check RUNNING or STOPPED instances (skip PROVISIONING, ERROR, DESTROYING)
  if (instance.status !== InstanceStatus.RUNNING && instance.status !== InstanceStatus.STOPPED) {
    throw new Error(`Instance ${instance.slug} is not checkable (status: ${instance.status})`);
  }

  const startTime = Date.now();
  let containers: ContainerInfo[];

  const driver = await getDriverForInstance(instance);

  try {
    containers = await driver.composePs(instance.basePath, instance.composeProject);
  } catch (err) {
    // If compose ps fails, record UNKNOWN status
    const updateData: { lastHealthCheck: Date; status?: InstanceStatus } = {
      lastHealthCheck: new Date(),
    };
    // If we thought it was RUNNING but can't even reach compose, mark as STOPPED
    if (instance.status === InstanceStatus.RUNNING) {
      updateData.status = InstanceStatus.STOPPED;
      logger.info(`[health] ${instance.slug}: auto-corrected status RUNNING → STOPPED (compose ps failed)`);
    }

    const healthCheck = await prisma.healthCheck.create({
      data: {
        instanceId,
        status: HealthStatus.UNKNOWN,
        serviceStatus: {},
        totalServices: 0,
        healthyServices: 0,
        responseTimeMs: Date.now() - startTime,
      },
    });

    await prisma.instance.update({
      where: { id: instanceId },
      data: updateData,
    });

    logger.warn(`[health] ${instance.slug}: compose ps failed: ${(err as Error).message}`);
    return healthCheck;
  }

  const responseTimeMs = Date.now() - startTime;
  const { status, serviceStatus, totalServices, healthyServices } = determineHealth(containers);

  // Auto-correct instance status based on actual container state
  const hasRunningContainers = containers.some((c) => c.state === 'running');

  if (instance.status === InstanceStatus.RUNNING && !hasRunningContainers) {
    await prisma.instance.update({
      where: { id: instanceId },
      data: {
        status: InstanceStatus.STOPPED,
        statusMessage: `No running containers detected at ${new Date().toISOString()}`,
      },
    });
    logger.info(`[health] ${instance.slug}: auto-corrected status RUNNING → STOPPED (0 running containers)`);
  } else if (instance.status === InstanceStatus.STOPPED && hasRunningContainers) {
    const runningCount = containers.filter((c) => c.state === 'running').length;
    await prisma.instance.update({
      where: { id: instanceId },
      data: {
        status: InstanceStatus.RUNNING,
        statusMessage: `${runningCount} container(s) running — detected at ${new Date().toISOString()}`,
      },
    });
    logger.info(`[health] ${instance.slug}: auto-corrected status STOPPED → RUNNING (${runningCount} running containers detected)`);
  }

  // Sync domain and feature flags from .env if they have drifted
  const envVars = instance.isRemote
    ? await driver.readEnvFile(instance.basePath)
    : await readEnvFile(instance.basePath);
  if (envVars) {
    const driftUpdates: Record<string, unknown> = {};

    // Domain sync
    const envDomain = envVars.DOMAIN;
    if (envDomain && envDomain !== instance.domain) {
      driftUpdates.domain = envDomain;
      logger.info(`[health] ${instance.slug}: synced domain ${instance.domain} → ${envDomain}`);
    }

    // Feature flag sync (only for registered/external instances)
    if (instance.isRegistered) {
      const envFlags = extractFeatureFlags(envVars);
      const flagKeys = Object.keys(envFlags) as Array<keyof typeof envFlags>;
      for (const key of flagKeys) {
        if ((instance as Record<string, unknown>)[key] !== envFlags[key]) {
          driftUpdates[key] = envFlags[key];
        }
      }
      if (Object.keys(driftUpdates).length > (envDomain && envDomain !== instance.domain ? 1 : 0)) {
        const changedFlags = flagKeys.filter(k => (instance as Record<string, unknown>)[k] !== envFlags[k]);
        if (changedFlags.length > 0) {
          logger.info(`[health] ${instance.slug}: synced feature flags: ${changedFlags.join(', ')}`);
        }
      }
    }

    if (Object.keys(driftUpdates).length > 0) {
      await prisma.instance.update({
        where: { id: instanceId },
        data: driftUpdates,
      });
    }
  }

  // Get the previous health check to detect transitions
  const previousCheck = await prisma.healthCheck.findFirst({
    where: { instanceId },
    orderBy: { checkedAt: 'desc' },
    select: { status: true },
  });

  const healthCheck = await prisma.healthCheck.create({
    data: {
      instanceId,
      status,
      serviceStatus,
      totalServices,
      healthyServices,
      responseTimeMs,
    },
  });

  const healthUpdateData: Record<string, unknown> = { lastHealthCheck: new Date() };
  if (instance.isRemote) {
    healthUpdateData.agentLastSeen = new Date();
  }
  await prisma.instance.update({
    where: { id: instanceId },
    data: healthUpdateData,
  });

  // Create events on health transitions
  const previousStatus = previousCheck?.status;
  if (status !== previousStatus) {
    if (status === HealthStatus.UNHEALTHY) {
      createEvent(
        instanceId,
        'ERROR',
        'health_check',
        'Instance unhealthy',
        `${instance.slug}: ${healthyServices}/${totalServices} services healthy`,
        { serviceStatus, responseTimeMs }
      ).catch((e) => logger.warn(`[health] Failed to create event: ${(e as Error).message}`));
    } else if (status === HealthStatus.DEGRADED && previousStatus !== HealthStatus.UNHEALTHY) {
      createEvent(
        instanceId,
        'WARNING',
        'health_check',
        'Instance degraded',
        `${instance.slug}: ${healthyServices}/${totalServices} services healthy`,
        { serviceStatus, responseTimeMs }
      ).catch((e) => logger.warn(`[health] Failed to create event: ${(e as Error).message}`));
    }
  }

  // Detect crashed containers (exited with non-zero exit code)
  for (const c of containers) {
    if (c.state === 'exited' && c.exitCode !== 0) {
      createEvent(
        instanceId,
        'ERROR',
        'container',
        `Container crashed: ${c.service || c.name}`,
        `${c.service || c.name} exited with code ${c.exitCode}`,
        { service: c.service, container: c.name, exitCode: c.exitCode, status: c.status }
      ).catch((e) => logger.warn(`[health] Failed to create container event: ${(e as Error).message}`));
    }
  }

  return healthCheck;
}

/**
 * Check all checkable instances (RUNNING + STOPPED) sequentially.
 * STOPPED instances are checked so we can detect when they come back online.
 */
export async function checkAllInstances(): Promise<void> {
  const instances = await prisma.instance.findMany({
    where: { status: { in: [InstanceStatus.RUNNING, InstanceStatus.STOPPED] } },
    select: { id: true, slug: true, status: true },
  });

  if (instances.length === 0) {
    logger.debug('[health] No checkable instances');
    return;
  }

  let healthy = 0;
  let degraded = 0;
  let unhealthy = 0;

  for (const inst of instances) {
    try {
      const check = await checkInstanceHealth(inst.id);
      if (check.status === HealthStatus.HEALTHY) healthy++;
      else if (check.status === HealthStatus.DEGRADED) degraded++;
      else unhealthy++;
    } catch (err) {
      logger.warn(`[health] Failed to check ${inst.slug}: ${(err as Error).message}`);
      unhealthy++;
    }
  }

  logger.info(
    `[health] Checked ${instances.length} instances: ${healthy} healthy, ${degraded} degraded, ${unhealthy} unhealthy`
  );
}

/**
 * Get paginated health history for an instance.
 */
export async function getHealthHistory(instanceId: string, page = 1, limit = 20) {
  const [data, total] = await Promise.all([
    prisma.healthCheck.findMany({
      where: { instanceId },
      orderBy: { checkedAt: 'desc' },
      skip: (page - 1) * limit,
      take: limit,
    }),
    prisma.healthCheck.count({ where: { instanceId } }),
  ]);

  return { data, total, page, limit };
}

/**
 * Start the periodic health check scheduler.
 */
export function startHealthScheduler(intervalMs: number): NodeJS.Timeout | null {
  if (intervalMs <= 0) {
    logger.info('[health] Automated health checks disabled (interval=0)');
    return null;
  }

  logger.info(`[health] Starting health scheduler (interval: ${intervalMs}ms)`);

  // Run initial check after a short delay (let services start)
  setTimeout(() => {
    checkAllInstances().catch((err) =>
      logger.error(`[health] Initial check failed: ${(err as Error).message}`)
    );
  }, 10_000);

  return setInterval(() => {
    checkAllInstances().catch((err) =>
      logger.error(`[health] Scheduled check failed: ${(err as Error).message}`)
    );
  }, intervalMs);
}
