// Простой симулятор роутинга sing-box.
// Берём список customRules + safety/RU-direct и определяем, какое правило сработает.
// Для geoip-ru / geosite-category-ru возвращаем "может сработать" — без скачанного ruleset
// мы не можем точно сказать, попадает ли IP/домен в RU.

import net from "node:net";
import { TPROXY_INBOUND, MIXED_INBOUND } from "./devices.js";

function ipv4ToInt(ip) {
  const parts = ip.split(".").map((x) => Number.parseInt(x, 10));
  if (
    parts.length !== 4 ||
    parts.some((n) => Number.isNaN(n) || n < 0 || n > 255)
  )
    return null;
  return (
    ((parts[0] << 24) >>> 0) + (parts[1] << 16) + (parts[2] << 8) + parts[3]
  );
}

function ipInCidr(ip, cidr) {
  if (!net.isIP(ip)) return false;
  const [addr, maskStr] = String(cidr).split("/");
  if (!addr) return false;

  if (net.isIPv4(ip) && net.isIPv4(addr)) {
    const mask = maskStr === undefined ? 32 : Number.parseInt(maskStr, 10);
    if (!Number.isInteger(mask) || mask < 0 || mask > 32) return false;
    const ipInt = ipv4ToInt(ip);
    const cidrInt = ipv4ToInt(addr);
    if (ipInt === null || cidrInt === null) return false;
    if (mask === 0) return true;
    const m = (~0 << (32 - mask)) >>> 0;
    return (ipInt & m) === (cidrInt & m);
  }
  // IPv6 — упрощённо: точное сравнение строк (без полноценной обработки)
  return false;
}

const PRIVATE_CIDRS = [
  "10.0.0.0/8",
  "172.16.0.0/12",
  "192.168.0.0/16",
  "127.0.0.0/8",
  "169.254.0.0/16",
];

function isPrivateIp(ip) {
  if (!ip) return false;
  return PRIVATE_CIDRS.some((cidr) => ipInCidr(ip, cidr));
}

function normalizeCidr(ip) {
  const value = String(ip || "").trim();
  if (!value) return "";
  return value.includes("/") ? value : `${value}/32`;
}

function deviceMatchesSourceIp(device, sourceIp) {
  if (!device?.ip || !sourceIp) return false;
  return ipInCidr(sourceIp, normalizeCidr(device.ip));
}

function modeOutbound(mode, vpnTag) {
  if (mode === "vpn") return `${vpnTag} (VPN)`;
  if (mode === "direct" || mode === "block") return mode;
  return null;
}

function likelyRuHost(host) {
  const value = String(host || "").toLowerCase();
  return value === "ru" || value.endsWith(".ru");
}

function hostMatchesDomain(host, domain) {
  if (!host || !domain) return false;
  return host.toLowerCase() === domain.toLowerCase();
}

function hostMatchesSuffix(host, suffix) {
  if (!host || !suffix) return false;
  const h = host.toLowerCase();
  const s = suffix.toLowerCase();
  return h === s || h.endsWith("." + s) || h.endsWith(s);
}

function hostMatchesKeyword(host, keyword) {
  if (!host || !keyword) return false;
  return host.toLowerCase().includes(keyword.toLowerCase());
}

function ruleMatches(rule, target) {
  const { host = "", ip = "", port, network } = target;

  if (!rule?.enabled) return false;

  const checks = [];

  if (rule.domains?.length) {
    checks.push(rule.domains.some((d) => hostMatchesDomain(host, d)));
  }
  if (rule.domainSuffixes?.length) {
    checks.push(rule.domainSuffixes.some((d) => hostMatchesSuffix(host, d)));
  }
  if (rule.domainKeywords?.length) {
    checks.push(rule.domainKeywords.some((d) => hostMatchesKeyword(host, d)));
  }
  if (rule.ipCidrs?.length) {
    if (!ip) return false;
    checks.push(rule.ipCidrs.some((cidr) => ipInCidr(ip, cidr)));
  }
  if (rule.ports?.length) {
    if (port === undefined || port === null || port === "") return false;
    const p = Number(port);
    checks.push(
      rule.ports.some((portStr) => {
        const s = String(portStr).trim();
        if (s.includes("-")) {
          const [from, to] = s.split("-").map((x) => Number(x));
          return p >= from && p <= to;
        }
        return p === Number(s);
      }),
    );
  }
  if (rule.networks?.length) {
    if (!network) return false;
    checks.push(rule.networks.includes(network));
  }

  if (!checks.length) return false;
  return checks.every(Boolean);
}

/**
 * Симулирует роутинг и возвращает результат.
 * @param {object} target { host, ip, port, network }
 * @param {Array} customRules
 * @param {object} options { routingRuDirect, vpnTag }
 */
export function matchRoute(target, customRules, options = {}) {
  const {
    routingRuDirect = true,
    vpnTag = "vpn-out",
    deviceProfiles = {
      defaultTransparentMode: "vpn",
      proxyDefaultMode: "vpn",
      devices: [],
    },
  } = options;
  const rules = Array.isArray(customRules) ? customRules : [];
  const inbound = target.inbound || TPROXY_INBOUND;
  const sourceIp = target.sourceIp || "";
  const devices = Array.isArray(deviceProfiles.devices)
    ? deviceProfiles.devices
    : [];
  const matchedDevice = devices.find(
    (device) =>
      device.enabled !== false && deviceMatchesSourceIp(device, sourceIp),
  );

  if (
    inbound === TPROXY_INBOUND &&
    matchedDevice &&
    matchedDevice.mode === "bypass"
  ) {
    return {
      matched: "kernel-bypass",
      ruleIndex: -1,
      ruleId: matchedDevice.id,
      ruleName: `${matchedDevice.name} -> bypass TProxy`,
      outbound: "direct",
      reason: "Source IP исключён на уровне iptables до попадания в sing-box",
    };
  }

  // 1. private IP → direct
  if (target.ip && isPrivateIp(target.ip)) {
    return {
      matched: "system",
      ruleIndex: -1,
      ruleName: "private IP → direct",
      outbound: "direct",
      reason: `IP ${target.ip} приватный`,
    };
  }

  // 2. global custom rules apply to every inbound before fallbacks.
  for (let i = 0; i < rules.length; i += 1) {
    const rule = rules[i];
    if (ruleMatches(rule, target)) {
      const outbound =
        rule.outbound === "vpn" ? `${vpnTag} (VPN)` : rule.outbound;
      return {
        matched: "custom",
        ruleIndex: i,
        ruleId: rule.id,
        ruleName: rule.name,
        outbound,
        reason: "Совпадение по global custom rule",
      };
    }
  }

  // 3. RU direct is global. Without a local rule-set DB we only detect obvious .ru hosts.
  if (routingRuDirect && likelyRuHost(target.host)) {
    return {
      matched: "geo",
      ruleIndex: -2,
      ruleName: "geosite-category-ru → direct",
      outbound: "direct",
      reason: "Домен выглядит как RU; точное попадание в rule-set проверит sing-box",
    };
  }

  // 4. transparent device defaults.
  if (inbound === TPROXY_INBOUND && matchedDevice) {
    const outbound = modeOutbound(matchedDevice.mode, vpnTag);
    if (outbound) {
      return {
        matched: "device-default",
        ruleIndex: -1,
        ruleId: matchedDevice.id,
        ruleName: `${matchedDevice.name} → ${matchedDevice.mode}`,
        outbound,
        reason: "Fallback устройства после global rules",
      };
    }
  }

  // 5. explicit proxy default.
  if (inbound === MIXED_INBOUND) {
    const mode = deviceProfiles.proxyDefaultMode || "vpn";
    return {
      matched: "proxy-default",
      ruleIndex: -1,
      ruleName: `mixed-in default → ${mode}`,
      outbound: modeOutbound(mode, vpnTag) || `${vpnTag} (VPN)`,
      reason: "Fallback explicit HTTP/SOCKS proxy после global rules",
    };
  }

  // 6. unknown transparent device default.
  if (inbound === TPROXY_INBOUND) {
    const mode = deviceProfiles.defaultTransparentMode || "vpn";
    return {
      matched: "transparent-default",
      ruleIndex: -1,
      ruleName: `transparent default → ${mode}`,
      outbound: modeOutbound(mode, vpnTag) || "direct",
      reason: "Fallback unknown transparent device после global rules",
    };
  }

  // 7. final → direct
  return {
    matched: "final",
    ruleIndex: -3,
    ruleName: "final",
    outbound: "direct",
    reason: "Не сработало ни одно правило — итоговый final отправляет напрямую",
  };
}

/**
 * Детектор конфликтов: ищет правила, перекрытые предыдущими.
 * Простая эвристика: если правило-кандидат полностью перекрывается ранее идущим
 * по доменам/суффиксам/CIDR — отмечаем конфликт.
 */
export function detectRuleConflicts(rules) {
  const list = Array.isArray(rules) ? rules : [];
  const conflicts = [];

  for (let i = 1; i < list.length; i += 1) {
    const cur = list[i];
    if (!cur?.enabled) continue;

    for (let j = 0; j < i; j += 1) {
      const prev = list[j];
      if (!prev?.enabled) continue;

      // Если outbound одинаковый — это не "конфликт", это дубликат
      const sameOutbound = prev.outbound === cur.outbound;

      // Проверка перекрытия доменов
      const overlaps = [];

      // Точные домены покрываются prev.suffix
      for (const d of cur.domains || []) {
        if ((prev.domainSuffixes || []).some((s) => hostMatchesSuffix(d, s))) {
          overlaps.push({
            kind: "domain",
            value: d,
            by: `суффикс ${(prev.domainSuffixes || []).find((s) => hostMatchesSuffix(d, s))}`,
          });
        }
        if ((prev.domains || []).includes(d)) {
          overlaps.push({ kind: "domain", value: d, by: "точный домен" });
        }
      }

      // Суффиксы покрываются более общим суффиксом prev
      for (const s of cur.domainSuffixes || []) {
        if (
          (prev.domainSuffixes || []).some(
            (ps) => hostMatchesSuffix(s, ps) && ps !== s,
          )
        ) {
          overlaps.push({
            kind: "suffix",
            value: s,
            by: "более общий суффикс",
          });
        }
      }

      // CIDR
      for (const c of cur.ipCidrs || []) {
        if ((prev.ipCidrs || []).includes(c)) {
          overlaps.push({ kind: "cidr", value: c, by: "тот же CIDR" });
        }
      }

      if (overlaps.length) {
        conflicts.push({
          ruleId: cur.id,
          ruleIndex: i,
          ruleName: cur.name,
          conflictWithId: prev.id,
          conflictWithIndex: j,
          conflictWithName: prev.name,
          severity: sameOutbound ? "info" : "warning",
          overlaps,
        });
      }
    }
  }

  return conflicts;
}
