import { execFile } from "node:child_process";
import { promisify } from "node:util";
import https from "node:https";
import tls from "node:tls";
import { getConfig, SSH_KEY_PATH, SSH_TIMEOUT } from "../config.js";
import { setSite } from "../cache.js";
import type { SiteStatus } from "../types.js";

const NOTIFICATIONS_URL = process.env.NOTIFICATIONS_SERVICE_URL || "http://127.0.0.1:8087";
const previousSiteStatus = new Map<string, string>();

function postSiteNotification(title: string, description: string, level: string): void {
  fetch(`${NOTIFICATIONS_URL}/notifications`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ title, description, source: "status", level }),
  }).catch(console.error);
}

const execFileAsync = promisify(execFile);

let discoveredSites: { id: string; name: string; url: string }[] = [];

export async function discoverSites(): Promise<void> {
  const config = getConfig();
  if (config.sites !== "auto") {
    discoveredSites = config.sites;
    return;
  }

  // Find the first SSH host that may be running nginx
  const nginxHost = config.hosts.find((h) => h.type === "ssh");
  if (!nginxHost) {
    console.warn("No SSH hosts in config — cannot auto-discover sites");
    return;
  }

  try {
    const { stdout } = await execFileAsync("ssh", [
      "-i", SSH_KEY_PATH,
      "-o", "StrictHostKeyChecking=accept-new",
      "-o", `ConnectTimeout=${SSH_TIMEOUT}`,
      "-o", "BatchMode=yes",
      `${nginxHost.user}@${nginxHost.ip}`,
      "sudo nginx -T 2>/dev/null | grep 'server_name' | grep -v '#' | sort -u",
    ], { timeout: 15_000 });

    const hostnames = new Set<string>();
    for (const line of stdout.split("\n")) {
      const match = line.match(/server_name\s+(.+);/);
      if (match) {
        for (const name of match[1].split(/\s+/)) {
          const clean = name.trim();
          if (clean && clean !== "_" && !clean.startsWith("$") && !clean.startsWith("www.")) {
            hostnames.add(clean);
          }
        }
      }
    }

    discoveredSites = Array.from(hostnames).map((hostname) => ({
      id: hostname.replace(/[^a-z0-9]/gi, "-").toLowerCase(),
      name: hostname,
      url: `https://${hostname}`,
    }));

    console.log(`Discovered ${discoveredSites.length} sites from nginx config`);
  } catch (e) {
    console.error("Failed to discover sites:", (e as Error).message);
  }
}

function checkSslExpiry(hostname: string): Promise<{ expiryDate: string | null; daysRemaining: number | null }> {
  return new Promise((resolve) => {
    try {
      const socket = tls.connect({ host: hostname, port: 443, servername: hostname, timeout: 5000 }, () => {
        const cert = socket.getPeerCertificate();
        socket.destroy();
        if (cert && cert.valid_to) {
          const expiry = new Date(cert.valid_to);
          const days = Math.floor((expiry.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
          resolve({ expiryDate: expiry.toISOString(), daysRemaining: days });
        } else {
          resolve({ expiryDate: null, daysRemaining: null });
        }
      });
      socket.on("error", () => { socket.destroy(); resolve({ expiryDate: null, daysRemaining: null }); });
      socket.on("timeout", () => { socket.destroy(); resolve({ expiryDate: null, daysRemaining: null }); });
    } catch {
      resolve({ expiryDate: null, daysRemaining: null });
    }
  });
}

function httpCheck(url: string): Promise<{ statusCode: number; responseTimeMs: number }> {
  return new Promise((resolve) => {
    const start = Date.now();
    const req = https.get(url, { timeout: 10_000, rejectUnauthorized: false }, (res) => {
      res.resume();
      resolve({ statusCode: res.statusCode || 0, responseTimeMs: Date.now() - start });
    });
    req.on("error", () => resolve({ statusCode: 0, responseTimeMs: Date.now() - start }));
    req.on("timeout", () => { req.destroy(); resolve({ statusCode: 0, responseTimeMs: Date.now() - start }); });
  });
}

function evaluateSiteHealth(statusCode: number, responseTimeMs: number, sslDaysRemaining: number | null): SiteStatus["status"] {
  if (statusCode === 0 || statusCode >= 500) return "down";
  if (responseTimeMs > 5000) return "down";
  if (responseTimeMs > 2000) return "degraded";
  if (sslDaysRemaining !== null && sslDaysRemaining < 7) return "down";
  if (sslDaysRemaining !== null && sslDaysRemaining < 30) return "degraded";
  return "up";
}

async function collectSite(site: { id: string; name: string; url: string }): Promise<void> {
  const now = new Date().toISOString();

  try {
    const [httpResult, sslResult] = await Promise.all([
      httpCheck(site.url),
      checkSslExpiry(site.name),
    ]);

    const newStatus = evaluateSiteHealth(httpResult.statusCode, httpResult.responseTimeMs, sslResult.daysRemaining);
    setSite({
      id: site.id,
      name: site.name,
      url: site.url,
      status: newStatus,
      response_time_ms: httpResult.responseTimeMs,
      http_status_code: httpResult.statusCode,
      ssl_expiry_date: sslResult.expiryDate,
      ssl_days_remaining: sslResult.daysRemaining,
      last_checked: now,
    });

    const prev = previousSiteStatus.get(site.id);
    previousSiteStatus.set(site.id, newStatus);
    if (prev !== undefined && prev !== "down" && newStatus === "down") {
      postSiteNotification(`Site Down: ${site.name}`, `HTTP ${httpResult.statusCode || 0} — ${site.url}`, "critical");
    } else if (prev === "down" && newStatus === "up") {
      postSiteNotification(`Site Recovered: ${site.name}`, `${site.url} is back up`, "info");
    }
  } catch (e) {
    setSite({
      id: site.id,
      name: site.name,
      url: site.url,
      status: "down",
      response_time_ms: 0,
      http_status_code: 0,
      ssl_expiry_date: null,
      ssl_days_remaining: null,
      last_checked: now,
    });

    const prev = previousSiteStatus.get(site.id);
    previousSiteStatus.set(site.id, "down");
    if (prev !== undefined && prev !== "down") {
      postSiteNotification(`Site Down: ${site.name}`, `${site.url} — ${(e as Error).message}`, "critical");
    }
    console.error(`Failed to check site ${site.name}:`, (e as Error).message);
  }
}

export async function collectAllSites(): Promise<void> {
  if (discoveredSites.length === 0) await discoverSites();

  const batchSize = 5;
  for (let i = 0; i < discoveredSites.length; i += batchSize) {
    const batch = discoveredSites.slice(i, i + batchSize);
    await Promise.allSettled(batch.map((s) => collectSite(s)));
  }
}
