import { exec } from "node:child_process";
import { promisify } from "node:util";
import { logger } from "./logger";

const execAsync = promisify(exec);

type ClientInfo = {
  port: number | null;
  pid: number | null;
  cwd: string | null;
  projectFolder: string | null;
};

type CacheEntry = ClientInfo & {
  expiresAt: number;
};

// Cache entries: port -> { pid, cwd, projectFolder, expiresAt }
const cache = new Map<number, CacheEntry>();
const CACHE_TTL_MS = 5 * 60 * 1000;
const MAX_CACHE_SIZE = 200;

// Deduplicate concurrent lookups for the same port
const inflight = new Map<number, Promise<ClientInfo>>();

function evictCacheIfNeeded(): void {
  while (cache.size > MAX_CACHE_SIZE) {
    const oldestKey = cache.keys().next().value;
    if (oldestKey === undefined) break;
    cache.delete(oldestKey);
  }
}

function getFromCache(port: number): ClientInfo | null {
  const entry = cache.get(port);
  if (entry && Date.now() < entry.expiresAt) {
    return { port: entry.port, pid: entry.pid, cwd: entry.cwd, projectFolder: entry.projectFolder };
  }
  if (entry) cache.delete(port);
  return null;
}

function setCache(port: number, info: ClientInfo): void {
  cache.set(port, { ...info, expiresAt: Date.now() + CACHE_TTL_MS });
  evictCacheIfNeeded();
}

/**
 * Get the remote port from a Request's underlying socket.
 * Works with Bun, Node.js, and Nitro's event.node.req.
 */
function extractRemotePort(request: Request): number | null {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
  const socket = (request as any).socket;
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
  const remotePort = (socket as any)?.remotePort;
  // eslint-disable-next-line @typescript-eslint/no-unsafe-return
  if (remotePort !== undefined && remotePort !== null) return remotePort;
  return null;
}

/**
 * Combined lookup: PID from port, then process info from PID.
 * On Windows, uses a single PowerShell call to get PID + command line.
 */
async function lookupClientInfo(port: number): Promise<ClientInfo> {
  const platform = process.platform;

  try {
    if (platform === "win32") {
      // Single PowerShell call: get PID and command line in one shot
      const psScript = [
        `$conn = Get-NetTCPConnection -LocalPort ${port} -State Established -ErrorAction SilentlyContinue | Select-Object -First 1`,
        `if ($conn) {`,
        `  $pid = $conn.OwningProcess`,
        `  $proc = Get-CimInstance Win32_Process -Filter "ProcessId=$pid" -ErrorAction SilentlyContinue`,
        `  $cmd = if ($proc) { $proc.CommandLine } else { "" }`,
        `  Write-Output "$pid|$cmd"`,
        `}`,
      ].join("; ");
      const { stdout } = await execAsync(`powershell -NoProfile -Command "${psScript}"`, {
        windowsHide: true,
      });
      const trimmed = stdout.trim();
      if (trimmed === "") {
        return { port, pid: null, cwd: null, projectFolder: null };
      }
      const [pidStr, ...cmdParts] = trimmed.split("|");
      const pid = parseInt(pidStr ?? "", 10);
      if (isNaN(pid) || pid <= 0) {
        return { port, pid: null, cwd: null, projectFolder: null };
      }
      const commandLine = cmdParts.join("|").trim();
      const { cwd, projectFolder } = parseCommandLine(commandLine, "\\");
      return { port, pid, cwd, projectFolder };
    }

    // Unix: two-step lookup (PID from port, then process info from PID)
    let pid: number | null = null;

    if (platform === "darwin") {
      const { stdout } = await execAsync(`lsof -i :${port} -sTCP:ESTABLISHED -t 2>/dev/null`, {
        shell: "/bin/sh",
      });
      const parsed = parseInt(stdout.trim(), 10);
      if (!isNaN(parsed) && parsed > 0) pid = parsed;
    } else {
      try {
        const { stdout } = await execAsync(
          `ss -tlnp 'sport = :${port}' 2>/dev/null | grep ESTAB | awk '{print $6}' | grep -o 'pid=[0-9]*' | cut -d= -f2`,
          { shell: "/bin/sh" },
        );
        const parsed = parseInt(stdout.trim(), 10);
        if (!isNaN(parsed) && parsed > 0) pid = parsed;
      } catch {
        // Fall back to netstat
      }
      if (pid === null) {
        const { stdout } = await execAsync(
          `netstat -tan 2>/dev/null | grep ':${port}' | grep ESTABLISHED | awk '{print $7}' | cut -d/ -f1`,
          { shell: "/bin/sh" },
        );
        const parsed = parseInt(stdout.trim(), 10);
        if (!isNaN(parsed) && parsed > 0) pid = parsed;
      }
    }

    if (pid === null) {
      return { port, pid: null, cwd: null, projectFolder: null };
    }

    const { cwd, projectFolder } = await lookupProcessInfo(pid);
    return { port, pid, cwd, projectFolder };
  } catch (err) {
    logger.debug(`[socketTracker] Failed to lookup info for port ${port}:`, String(err));
    return { port, pid: null, cwd: null, projectFolder: null };
  }
}

function parseCommandLine(
  commandLine: string,
  sep: string,
): { cwd: string | null; projectFolder: string | null } {
  if (!commandLine) return { cwd: null, projectFolder: null };
  const exeMatch = commandLine.match(/^"([^"]+)"/) || commandLine.match(/^([^\s]+)/);
  if (exeMatch?.[1] !== undefined) {
    const exePath = exeMatch[1];
    const lastSep = exePath.lastIndexOf(sep);
    if (lastSep > 0) {
      const exeDir = exePath.substring(0, lastSep);
      return { cwd: exeDir, projectFolder: exeDir.split(sep).pop() ?? null };
    }
  }
  return { cwd: null, projectFolder: null };
}

/**
 * Get the working directory and command line for a process by PID.
 */
async function lookupProcessInfo(
  pid: number,
): Promise<{ cwd: string | null; projectFolder: string | null }> {
  const platform = process.platform;

  try {
    if (platform === "win32") {
      const { stdout } = await execAsync(
        `wmic process where processid=${pid} get commandline /value`,
        { windowsHide: true },
      );
      const lines = stdout.trim().split("\n").filter(Boolean);
      for (const line of lines) {
        if (line.includes("=")) {
          const commandLine = (line.split("=")[1] ?? "").trim();
          if (commandLine) {
            return parseCommandLine(commandLine, "\\");
          }
        }
      }
    } else {
      try {
        const { stdout } = await execAsync(`cat /proc/${pid}/cmdline 2>/dev/null | tr '\\0' ' '`, {
          shell: "/bin/sh",
        });
        const commandLine = stdout.trim();
        if (commandLine) {
          return parseCommandLine(commandLine, "/");
        }
      } catch {
        // Fall back to ps
      }

      if (platform === "darwin") {
        const { stdout } = await execAsync(`ps -p ${pid} -o comm= 2>/dev/null`, {
          shell: "/bin/sh",
        });
        const exePath = stdout.trim();
        if (exePath) {
          return parseCommandLine(exePath, "/");
        }
      }
    }
    return { cwd: null, projectFolder: null };
  } catch {
    return { cwd: null, projectFolder: null };
  }
}

/**
 * Get client info (PID, CWD, project folder) from a request's source port.
 * Deduplicates concurrent lookups for the same port and caches results.
 */
export async function getClientInfo(request: Request): Promise<ClientInfo> {
  const port = extractRemotePort(request);
  if (port === null) {
    return { port: null, pid: null, cwd: null, projectFolder: null };
  }

  // Check cache first
  const cached = getFromCache(port);
  if (cached) return cached;

  // Deduplicate concurrent lookups for the same port
  const existing = inflight.get(port);
  if (existing) return existing;

  const promise = lookupClientInfo(port)
    .then((info) => {
      setCache(port, info);
      inflight.delete(port);
      return info;
    })
    .catch((err) => {
      inflight.delete(port);
      logger.debug(`[socketTracker] Lookup failed for port ${port}:`, String(err));
      const fallback: ClientInfo = { port, pid: null, cwd: null, projectFolder: null };
      setCache(port, fallback);
      return fallback;
    });

  inflight.set(port, promise);
  return promise;
}
