const https = require("https");
const tls = require("tls");
const http2 = require("http2");
const fs = require("fs");
const path = require("path");
const net = require("net");
const { getCertParams } = require("./cert/generate");
const { log, err, onLog, dumpRequest, createResponseDumper, clearDumpDir } = require("./logger");
const { DATA_DIR } = require("./paths");
const { URL_PATTERNS, MODEL_SYNONYMS, MODEL_PATTERNS, getToolForHost, LOG_BLACKLIST_URL_PARTS, IS_DEV, LSOF_BIN } = require("./config");
const { getMitmAlias } = require("./dbReader");
const { applyAntigravityIdeVersionOverride } = require("./antigravityIdeVersion");
const { removeAllDNSEntriesSync } = require("./dns/dnsConfig");
const { execSync } = require("child_process");

const antigravityHandler = require("./handlers/antigravity");
const copilotHandler = require("./handlers/copilot");

const IS_WIN = process.platform === "win32";

// Clean old dumps
if (IS_DEV) clearDumpDir();

// Write status for dashboard
function writeStatus(tool, method, url, status) {
  try {
    const statusFile = path.join(DATA_DIR, "mitm", "endpoint-status.json");
    let current = {};
    if (fs.existsSync(statusFile)) {
      current = JSON.parse(fs.readFileSync(statusFile, "utf8"));
    }
    
    if (!current[tool]) current[tool] = [];
    
    current[tool].unshift({
      timestamp: Date.now(),
      method,
      url,
      status
    });
    
    // Keep last 50
    current[tool] = current[tool].slice(0, 50);
    
    fs.writeFileSync(statusFile, JSON.stringify(current, null, 2));
  } catch (e) {
    // Ignore non-critical write errors
  }
}

// ─── Routing & Mapping ────────────────────────────────────────────────────────
const HOST_REWRITE = {
  "cloudcode-pa.googleapis.com": "daily-cloudcode-pa.googleapis.com", // Bypass strict rate limits
};

function determineTargetHost(reqHost) {
  const h = (reqHost || "").split(":")[0];
  return HOST_REWRITE[h] || h;
}

function matchUrlPattern(url, tool) {
  const patterns = URL_PATTERNS[tool];
  if (!patterns) return false;
  return patterns.some(p => url.includes(p));
}

function mapModel(rawModel, tool) {
  if (!rawModel || typeof rawModel !== "string") return "gemini-3.5-flash-low";
  
  // 1. Synonym Map (exact)
  if (MODEL_SYNONYMS[tool] && MODEL_SYNONYMS[tool][rawModel]) {
    return getMitmAlias(MODEL_SYNONYMS[tool][rawModel]) || rawModel;
  }
  
  // 2. Pattern Match
  if (MODEL_PATTERNS[tool]) {
    for (const rule of MODEL_PATTERNS[tool]) {
      if (rule.match.test(rawModel)) {
        return getMitmAlias(rule.alias) || rule.alias; // mapped to canonical, then to alias
      }
    }
  }
  
  // 3. Fallback
  return getMitmAlias(rawModel) || rawModel;
}

// ─── Passthrough ─────────────────────────────────────────────────────────────
async function passthrough(req, res, bodyBuffer, onResponse) {
  const dumper = IS_DEV ? createResponseDumper(req, "passthrough") : null;
  const isHttp2 = req.httpVersionMajor === 2;
  const targetHost = determineTargetHost(req.headers.host);
  
  const fwdHeaders = { ...req.headers, host: targetHost };
  // If we rewrote the body/metadata, content-length changed
  if (bodyBuffer) fwdHeaders["content-length"] = Buffer.byteLength(bodyBuffer);
  
  try {
    if (isHttp2) {
      return new Promise((resolve, reject) => {
        const client = http2.connect(`https://${targetHost}`);
        client.on("error", (err) => { reject(err); client.close(); });
        
        const pseudoHeaders = {
          ":method": req.method,
          ":path": req.url,
          ...fwdHeaders
        };
        delete pseudoHeaders.host;
        delete pseudoHeaders.connection;
        
        const proxyReq = client.request(pseudoHeaders);
        proxyReq.on("response", (headers, flags) => {
          if (onResponse) onResponse(headers, flags);
          if (dumper) dumper.writeHeader(headers[":status"], headers);
          res.writeHead(headers[":status"], headers);
        });
        proxyReq.on("data", (chunk) => {
          if (dumper) dumper.writeChunk(chunk);
          res.write(chunk);
        });
        proxyReq.on("end", () => {
          if (dumper) dumper.end();
          res.end();
          client.close();
          resolve();
        });
        proxyReq.on("error", (e) => {
          if (dumper) { dumper.writeChunk(`\n[ERROR] ${e.message}\n`); dumper.end(); }
          reject(e);
        });
        
        if (bodyBuffer) proxyReq.write(bodyBuffer);
        proxyReq.end();
      });
    } else {
      // HTTP/1.1 Passthrough
      return new Promise((resolve, reject) => {
        const options = {
          hostname: targetHost,
          port: 443,
          path: req.url,
          method: req.method,
          headers: fwdHeaders,
        };
        const proxyReq = https.request(options, (proxyRes) => {
          if (onResponse) onResponse(proxyRes.headers, null);
          if (dumper) dumper.writeHeader(proxyRes.statusCode, proxyRes.headers);
          res.writeHead(proxyRes.statusCode, proxyRes.headers);
          proxyRes.on("data", (chunk) => {
            if (dumper) dumper.writeChunk(chunk);
            res.write(chunk);
          });
          proxyRes.on("end", () => {
            if (dumper) dumper.end();
            res.end();
            resolve();
          });
        });
        proxyReq.on("error", (e) => {
          if (dumper) { dumper.writeChunk(`\n[ERROR] ${e.message}\n`); dumper.end(); }
          reject(e);
        });
        if (bodyBuffer) proxyReq.write(bodyBuffer);
        proxyReq.end();
      });
    }
  } catch (error) {
    if (dumper) { dumper.writeChunk(`\n[ERROR] ${error.message}\n`); dumper.end(); }
    if (!res.headersSent) res.writeHead(502);
    res.end();
  }
}

// ─── Main Request Handler ────────────────────────────────────────────────────
async function handleRequest(req, res) {
  // Arouter local log streaming
  if (req.url === "/_mitm_logs") {
    res.writeHead(200, {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      "Connection": "keep-alive",
      "Access-Control-Allow-Origin": "*"
    });
    
    const sendLog = (type, message) => {
      res.write(`data: ${JSON.stringify({ type, message })}\n\n`);
    };
    
    onLog((msg) => sendLog('log', msg));
    return;
  }

  const tool = getToolForHost(req.headers.host);
  const targetHost = determineTargetHost(req.headers.host);
  
  if (!tool) {
    return passthrough(req, res, null);
  }

  const isMuteLog = LOG_BLACKLIST_URL_PARTS.some(p => req.url.includes(p));

  const chunks = [];
  req.on("data", (chunk) => chunks.push(chunk));
  req.on("end", async () => {
    let bodyBuffer = Buffer.concat(chunks);
    let originalModel = null;
    let mappedModel = null;

    if (IS_DEV) dumpRequest(req, bodyBuffer, tool, targetHost);

    // Apply AG IDE Version Override
    if (tool === "antigravity") {
      const override = applyAntigravityIdeVersionOverride(bodyBuffer, req.headers);
      if (override.applied) {
        bodyBuffer = override.bodyBuffer;
        req.headers = override.headers; // mutated reference
        if (!isMuteLog) log(`[AG Override] Applied IDE version: ${override.version}`);
      }
    }

    if (matchUrlPattern(req.url, tool)) {
      try {
        const bodyStr = bodyBuffer.toString("utf8");
        const bodyObj = JSON.parse(bodyStr);
        originalModel = bodyObj.model || "gemini-3.5-flash-low";
        mappedModel = mapModel(originalModel, tool);

        if (!isMuteLog) {
          log(`[${tool}] Intercepting ${req.method} ${targetHost}${req.url.split("?")[0]}`);
          if (originalModel !== mappedModel) {
            log(`[${tool}] Model Map: ${originalModel} ➔ ${mappedModel}`);
          }
        }
        
        writeStatus(tool, req.method, req.url, "intercepted");
        
        if (tool === "antigravity") {
          await antigravityHandler.intercept(req, res, bodyBuffer, mappedModel);
        } else if (tool === "copilot") {
          await copilotHandler.intercept(req, res, bodyBuffer, mappedModel);
        } else {
          await passthrough(req, res, bodyBuffer);
        }
        
      } catch (e) {
        err(`[${tool}] Failed to parse body: ${e.message}`);
        writeStatus(tool, req.method, req.url, "error");
        await passthrough(req, res, bodyBuffer);
      }
    } else {
      if (!isMuteLog) {
        log(`[${tool}] Passthrough ${req.method} ${targetHost}${req.url.split("?")[0]}`);
      }
      writeStatus(tool, req.method, req.url, "passthrough");
      await passthrough(req, res, bodyBuffer);
    }
  });
  
  req.on("error", (e) => {
    err(`[${tool || "unknown"}] Request error: ${e.message}`);
  });
}

// ─── Server Setup ────────────────────────────────────────────────────────────
function start() {
  const certParams = getCertParams();
  if (!certParams || !certParams.cert) {
    err("Cannot start MITM: Certificate not found. Manager must generate it first.");
    process.exit(1);
  }
  
  // Read root CA to bundle with cert for full chain support (resolves SNI trust issues)
  let rootCAPem = "";
  try {
    if (fs.existsSync(certParams.certPath)) {
      rootCAPem = fs.readFileSync(certParams.certPath, "utf8");
    }
  } catch (e) {}

  const serverOptions = {
    ALPNProtocols: ["h2", "http/1.1"],
    SNICallback: (servername, cb) => {
      const ctx = tls.createSecureContext({
        key: certParams.key,
        cert: certParams.cert + "\n" + rootCAPem, // Full chain
      });
      cb(null, ctx);
    },
    allowHTTP1: true // for http2 backwards compatibility
  };

  let server;
  
  const requestListener = (req, res) => {
    handleRequest(req, res).catch(e => {
      err(`Unhandled request error: ${e.message}`);
      if (!res.headersSent) res.writeHead(500);
      res.end();
    });
  };

  try {
    server = http2.createSecureServer(serverOptions, requestListener);
  } catch (e) {
    // Fallback to https if http2 fails for any reason
    err(`HTTP/2 server creation failed, falling back to HTTPS: ${e.message}`);
    server = https.createServer(serverOptions, requestListener);
  }

  // Pre-emptively kill any stale process lingering on 443
  try {
    if (IS_WIN) {
      // Requires cmd wrapped execution due to pipe
    } else {
      if (LSOF_BIN) {
        execSync(`${LSOF_BIN} -t -i:443 | xargs kill -9 2>/dev/null || true`, { stdio: "ignore" });
      }
    }
  } catch (e) { /* ignore */ }

  server.on("error", (error) => {
    if (error.code === "EADDRINUSE") {
      err("Port 443 is already in use. Ensure no other server is running on this port.");
      removeAllDNSEntriesSync(); // Clean up DNS before exiting
      process.exit(1);
    } else {
      err(`Server error: ${error.message}`);
    }
  });

  server.listen(443, "127.0.0.1", () => {
    log("MITM Server listening on 127.0.0.1:443 (ALPN: h2, http/1.1)");
  });
}

// ─── Graceful Shutdown ───────────────────────────────────────────────────────
function cleanupAndExit() {
  log("Received shutdown signal. Cleaning up DNS...");
  removeAllDNSEntriesSync();
  process.exit(0);
}

process.on("SIGTERM", cleanupAndExit);
process.on("SIGINT", cleanupAndExit);
process.on("uncaughtException", (error) => {
  err(`Uncaught Exception: ${error.message}`);
  removeAllDNSEntriesSync();
  process.exit(1);
});

start();
