import fs from "node:fs";
import path from "node:path";
import { settings } from "./config.js";
import {
  MIXED_INBOUND,
  TPROXY_INBOUND,
  normalizeCidr,
  readDeviceProfiles,
} from "./devices.js";
import { readClientSettings } from "./clientSettings.js";

const PROXY_TYPES = new Set([
  "vless",
  "vmess",
  "trojan",
  "shadowsocks",
  "hysteria2",
]);
const CUSTOM_OUTBOUNDS = new Set(["direct", "vpn", "block"]);

function clone(value) {
  return JSON.parse(JSON.stringify(value));
}

function findOutbound(subscriptionConfig, selectedTag) {
  const outbounds = Array.isArray(subscriptionConfig?.outbounds)
    ? subscriptionConfig.outbounds
    : [];
  const exact = outbounds.find(
    (outbound) =>
      outbound.tag === selectedTag && PROXY_TYPES.has(outbound.type),
  );
  if (exact) return exact;

  const trimmedTag = String(selectedTag || "").trim();
  return outbounds.find(
    (outbound) =>
      String(outbound.tag || "").trim() === trimmedTag &&
      PROXY_TYPES.has(outbound.type),
  );
}

function readCustomRuleSets() {
  try {
    if (!fs.existsSync(settings.customRuleSetsPath)) return [];
    const data = JSON.parse(
      fs.readFileSync(settings.customRuleSetsPath, "utf8"),
    );
    return Array.isArray(data) ? data : [];
  } catch {
    return [];
  }
}

function ruleSetDownloadDetour(vpnTag) {
  const detour = String(settings.ruleSetDownloadDetour || "vpn").trim();
  if (!detour || detour === "vpn") return vpnTag;
  return detour;
}

function ruleSets(customRuleSets = [], vpnTag = "direct") {
  const downloadDetour = ruleSetDownloadDetour(vpnTag);
  const builtIn = settings.routingRuDirect
    ? [
        {
          type: "remote",
          tag: "geoip-ru",
          format: "binary",
          url: "https://cdn.jsdelivr.net/gh/SagerNet/sing-geoip@rule-set/geoip-ru.srs",
          download_detour: downloadDetour,
        },
        {
          type: "remote",
          tag: "geosite-category-ru",
          format: "binary",
          url: "https://cdn.jsdelivr.net/gh/SagerNet/sing-geosite@rule-set/geosite-category-ru.srs",
          download_detour: downloadDetour,
        },
      ]
    : [];

  const custom = (Array.isArray(customRuleSets) ? customRuleSets : [])
    .filter((rs) => rs.tag && rs.url)
    .map((rs) => ({
      type: "remote",
      tag: String(rs.tag).trim(),
      format: rs.format || "binary",
      url: String(rs.url).trim(),
      download_detour: downloadDetour,
    }));

  // Пользовательские rule-sets не должны дублировать встроенные
  const builtInTags = new Set(builtIn.map((rs) => rs.tag));
  const merged = [
    ...builtIn,
    ...custom.filter((rs) => !builtInTags.has(rs.tag)),
  ];
  return merged;
}

function uniqueClean(values) {
  return Array.from(
    new Set(
      (Array.isArray(values) ? values : [])
        .map((value) => String(value || "").trim())
        .filter(Boolean),
    ),
  );
}

function parsePorts(values) {
  return uniqueClean(values)
    .map((value) => Number.parseInt(value, 10))
    .filter((value) => Number.isInteger(value) && value > 0 && value <= 65535);
}

function toSingboxRule(customRule, vpnTag, baseRule = {}) {
  if (!customRule?.enabled) return null;
  if (!CUSTOM_OUTBOUNDS.has(customRule.outbound)) return null;

  const rule = { ...baseRule };
  const domains = uniqueClean(customRule.domains);
  const domainSuffixes = uniqueClean(customRule.domainSuffixes);
  const domainKeywords = uniqueClean(customRule.domainKeywords);
  const ipCidrs = uniqueClean(customRule.ipCidrs);
  const ports = parsePorts(customRule.ports);
  const networks = uniqueClean(customRule.networks).filter((network) =>
    ["tcp", "udp"].includes(network),
  );

  if (domains.length) rule.domain = domains;
  if (domainSuffixes.length) rule.domain_suffix = domainSuffixes;
  if (domainKeywords.length) rule.domain_keyword = domainKeywords;
  if (ipCidrs.length) rule.ip_cidr = ipCidrs;
  if (ports.length) rule.port = ports;
  if (networks.length) rule.network = networks;

  const ruleSetsRef = uniqueClean(customRule.ruleSets);
  if (ruleSetsRef.length) rule.rule_set = ruleSetsRef;

  if (
    !rule.domain &&
    !rule.domain_suffix &&
    !rule.domain_keyword &&
    !rule.ip_cidr &&
    !rule.port &&
    !rule.network &&
    !rule.rule_set
  ) {
    return null;
  }

  rule.outbound = customRule.outbound === "vpn" ? vpnTag : customRule.outbound;
  return rule;
}

function customRouteRules(customRules, vpnTag, baseRule = {}) {
  return (Array.isArray(customRules) ? customRules : [])
    .map((rule) => toSingboxRule(rule, vpnTag, baseRule))
    .filter(Boolean);
}

// ─── Device rules (маршрутизация по source IP) ──────────────────────────────

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

function deviceDefaultRouteRule(device, vpnTag) {
  if (!device?.enabled) return null;
  const outbound = modeOutbound(device.mode, vpnTag);
  if (!outbound) return null;

  const cidr = normalizeCidr(device.ip);
  if (!cidr) return null;

  return {
    inbound: [TPROXY_INBOUND],
    source_ip_cidr: [cidr],
    outbound,
  };
}

function deviceDefaultRouteRules(devices, vpnTag) {
  return (Array.isArray(devices) ? devices : [])
    .map((device) => deviceDefaultRouteRule(device, vpnTag))
    .filter(Boolean);
}

function inboundDefaultRule(inbound, mode, vpnTag) {
  const outbound = modeOutbound(mode, vpnTag);
  if (!outbound) return null;
  return { inbound: [inbound], outbound };
}

function ruDirectRule() {
  if (!settings.routingRuDirect) return null;
  return {
    rule_set: ["geoip-ru", "geosite-category-ru"],
    outbound: "direct",
  };
}

function routeRules(customRules, vpnTag, { includeTransparent = true } = {}) {
  const deviceProfiles = readDeviceProfiles();
  const rules = [
    {
      ip_is_private: true,
      outbound: "direct",
    },
  ];

  // Global rules apply to every inbound before contextual fallbacks.
  rules.push(...customRouteRules(customRules, vpnTag));

  const ruRule = ruDirectRule();
  if (ruRule) rules.push(ruRule);

  if (includeTransparent) {
    // Device defaults are only transparent-gateway fallbacks after global rules.
    rules.push(...deviceDefaultRouteRules(deviceProfiles.devices, vpnTag));
  }

  const proxyFallback = inboundDefaultRule(
    MIXED_INBOUND,
    deviceProfiles.proxyDefaultMode,
    vpnTag,
  );
  if (proxyFallback) rules.push(proxyFallback);

  if (includeTransparent) {
    const transparentFallback = inboundDefaultRule(
      TPROXY_INBOUND,
      deviceProfiles.defaultTransparentMode,
      vpnTag,
    );
    if (transparentFallback) rules.push(transparentFallback);
  }

  return rules;
}

function sharedProxyOutbound(sharedProxy) {
  if (!sharedProxy?.host || !sharedProxy?.port) return null;
  if (sharedProxy.protocol === "http") {
    return {
      type: "http",
      tag: "shared-proxy",
      server: sharedProxy.host,
      server_port: sharedProxy.port,
    };
  }
  return {
    type: "socks",
    tag: "shared-proxy",
    server: sharedProxy.host,
    server_port: sharedProxy.port,
    version: "5",
  };
}

export function buildGatewayConfig(
  subscriptionConfig,
  selectedTag,
  { bypassAll = false } = {},
) {
  const customRuleSets = readCustomRuleSets();
  const clientMode = settings.appMode === "client";
  const clientSettings = clientMode ? readClientSettings() : null;
  const sharedOutbound =
    clientMode && clientSettings?.sharedProxyEnabled
      ? sharedProxyOutbound(clientSettings.sharedProxy)
      : null;
  const directOnlyClient = clientMode && clientSettings?.homeBypassEnabled;
  const selectedOutbound = sharedOutbound
    ? null
    : findOutbound(subscriptionConfig, selectedTag);
  if (!sharedOutbound && !directOnlyClient && !selectedOutbound) {
    throw new Error(`Outbound не найден: ${selectedTag}`);
  }

  const vpnOutbound = selectedOutbound ? clone(selectedOutbound) : null;
  if (vpnOutbound && !vpnOutbound.tag) vpnOutbound.tag = "vpn-out";
  if (vpnOutbound?.type === "vless" && !vpnOutbound.packet_encoding) {
    vpnOutbound.packet_encoding = "xudp";
  }

  const clientOutbound = sharedOutbound
    ? sharedOutbound.tag
    : clientSettings?.homeBypassEnabled
      ? "direct"
      : vpnOutbound.tag;
  const mixedProxyPort = clientSettings?.proxyPort || settings.proxyPort;
  const proxyOnlyRules = [{ inbound: [MIXED_INBOUND], outbound: clientOutbound }];
  const inbounds = [
    ...(clientMode
      ? []
      : [
          {
            type: "tproxy",
            tag: "tproxy-in",
            listen: "::",
            listen_port: settings.tproxyPort,
            sniff: true,
            sniff_override_destination: true,
          },
        ]),
    {
      type: "mixed",
      tag: "mixed-in",
      listen: settings.bindIp,
      listen_port: mixedProxyPort,
      sniff: true,
      set_system_proxy: false,
    },
  ];

  return {
    log: {
      level: settings.logLevel,
      timestamp: true,
    },
    experimental: {
      cache_file: {
        enabled: true,
        path: settings.cachePath,
      },
    },
    dns: {
      independent_cache: true,
    },
    inbounds,
    outbounds: [
      ...(sharedOutbound ? [sharedOutbound] : vpnOutbound ? [vpnOutbound] : []),
      { type: "direct", tag: "direct" },
      { type: "block", tag: "block" },
    ],
    route: {
      rule_set: bypassAll || clientMode ? [] : ruleSets(customRuleSets, vpnOutbound.tag),
      rules: bypassAll
        ? [{ ip_is_private: true, outbound: "direct" }]
        : clientMode
          ? proxyOnlyRules
        : routeRules(subscriptionConfig.customRules, vpnOutbound.tag, {
            includeTransparent: !clientMode,
          }),
      final: "direct",
      ...(clientMode ? {} : { auto_detect_interface: true }),
    },
  };
}

export function writeSingboxConfig(config) {
  fs.mkdirSync(path.dirname(settings.configPath), { recursive: true });
  fs.writeFileSync(
    settings.configPath,
    JSON.stringify(config, null, 2),
    "utf8",
  );
}

export function readSingboxConfig() {
  if (!fs.existsSync(settings.configPath)) return null;
  try {
    return JSON.parse(fs.readFileSync(settings.configPath, "utf8"));
  } catch {
    return null;
  }
}

export function removeSingboxConfig() {
  if (fs.existsSync(settings.configPath)) {
    fs.rmSync(settings.configPath);
  }
}
