import { XHttpXmuxSchema } from '@/schemas/protocols/stream/xhttp';
import { OutboundDomainStrategySchema } from '@/schemas/protocols/outbound';
import { normalizeStreamSettingsForWire } from '@/lib/xray/stream-wire-normalize';
import { Wireguard } from '@/utils';
import type { Sniffing, SniffingDest } from '@/schemas/primitives';
import type { OutboundDomainStrategy } from '@/schemas/protocols/outbound';

import type {
  BlackholeOutboundFormSettings,
  DnsOutboundFormSettings,
  DnsRuleForm,
  FreedomFinalRuleForm,
  FreedomOutboundFormSettings,
  HttpOutboundFormSettings,
  HysteriaOutboundFormSettings,
  LoopbackOutboundFormSettings,
  MuxForm,
  OutboundFormSettings,
  OutboundFormValues,
  OutboundStreamFormValues,
  ShadowsocksOutboundFormSettings,
  TrojanOutboundFormSettings,
  VlessOutboundFormSettings,
  VmessOutboundFormSettings,
  WireguardOutboundFormPeer,
  WireguardOutboundFormSettings,
} from '@/schemas/forms/outbound-form';

type Raw = Record<string, unknown>;

function asObject(value: unknown): Raw {
  return value && typeof value === 'object' && !Array.isArray(value) ? (value as Raw) : {};
}

function asArray(value: unknown): unknown[] {
  return Array.isArray(value) ? value : [];
}

function asString(value: unknown, fallback = ''): string {
  return typeof value === 'string' ? value : fallback;
}

function asNumber(value: unknown, fallback = 0): number {
  if (typeof value === 'number' && Number.isFinite(value)) return value;
  if (typeof value === 'string' && value.trim() !== '') {
    const n = Number(value);
    return Number.isFinite(n) ? n : fallback;
  }
  return fallback;
}

function asBool(value: unknown): boolean {
  return value === true;
}

function asPort(value: unknown, fallback: number): number {
  const n = asNumber(value, fallback);
  if (!Number.isInteger(n) || n < 1 || n > 65535) return fallback;
  return n;
}

// xray-core matches targetStrategy/domainStrategy case-insensitively;
// normalize the wire value to the canonical spelling or '' (= AsIs).
function targetStrategyFromWire(value: unknown): OutboundDomainStrategy | '' {
  const s = asString(value);
  if (!s) return '';
  return (
    OutboundDomainStrategySchema.options.find((v) => v.toLowerCase() === s.toLowerCase()) ?? ''
  );
}

const SNIFFING_DEST_VALUES: readonly SniffingDest[] = ['http', 'tls', 'quic', 'fakedns'];

const SNIFFING_DEFAULT: Sniffing = {
  enabled: false,
  destOverride: [...SNIFFING_DEST_VALUES],
  metadataOnly: false,
  routeOnly: false,
  ipsExcluded: [],
  domainsExcluded: [],
};

// Shared by VLESS reverse sniffing and the loopback outbound — both edit the
// same xray SniffingConfig. Unknown destOverride tokens are dropped so the
// value satisfies SniffingSchema's enum.
function sniffingFromWire(raw: unknown): Sniffing {
  const r = asObject(raw);
  const dest = asArray(r.destOverride)
    .map((x) => asString(x))
    .filter((x): x is SniffingDest => (SNIFFING_DEST_VALUES as readonly string[]).includes(x));
  return {
    enabled: asBool(r.enabled),
    destOverride: dest.length > 0 ? dest : [...SNIFFING_DEST_VALUES],
    metadataOnly: asBool(r.metadataOnly),
    routeOnly: asBool(r.routeOnly),
    ipsExcluded: asArray(r.ipsExcluded).map((x) => asString(x)),
    domainsExcluded: asArray(r.domainsExcluded).map((x) => asString(x)),
  };
}

function vmessFromWire(raw: Raw): VmessOutboundFormSettings {
  const vnext = asArray(raw.vnext);
  const v = asObject(vnext[0]);
  const u = asObject(asArray(v.users)[0]);
  return {
    address: asString(v.address),
    port: asPort(v.port, 443),
    id: asString(u.id),
    security: ((): VmessOutboundFormSettings['security'] => {
      const s = asString(u.security);
      const allowed = ['aes-128-gcm', 'chacha20-poly1305', 'auto'];
      return (allowed.includes(s) ? s : 'auto') as VmessOutboundFormSettings['security'];
    })(),
  };
}

function vlessFromWire(raw: Raw): VlessOutboundFormSettings {
  let address = asString(raw.address);
  let port = asPort(raw.port, 443);
  let id = asString(raw.id);
  let flow = asString(raw.flow);
  let encryption = asString(raw.encryption, 'none');
  const vnext = asArray(raw.vnext);
  if (vnext.length > 0) {
    const v = asObject(vnext[0]);
    const u = asObject(asArray(v.users)[0]);
    address = asString(v.address);
    port = asPort(v.port, 443);
    id = asString(u.id);
    flow = asString(u.flow);
    encryption = asString(u.encryption, 'none');
  }
  const reverse = asObject(raw.reverse);
  const reverseTag = asString(reverse.tag);
  const reverseSniffing = reverseTag ? sniffingFromWire(reverse.sniffing) : SNIFFING_DEFAULT;
  const savedSeed = asArray(raw.testseed);
  const testseed =
    savedSeed.length === 4 && savedSeed.every((n) => Number.isInteger(n) && (n as number) > 0)
      ? (savedSeed as number[])
      : [900, 500, 900, 256];
  return {
    address,
    port,
    id,
    flow,
    encryption: encryption || 'none',
    reverseTag,
    reverseSniffing,
    testpre: asNumber(raw.testpre, 0),
    testseed,
  };
}

function trojanFromWire(raw: Raw): TrojanOutboundFormSettings {
  const s = asObject(asArray(raw.servers)[0]);
  return {
    address: asString(s.address),
    port: asPort(s.port, 443),
    password: asString(s.password),
  };
}

function shadowsocksFromWire(raw: Raw): ShadowsocksOutboundFormSettings {
  const s = asObject(asArray(raw.servers)[0]);
  return {
    address: asString(s.address),
    port: asPort(s.port, 443),
    password: asString(s.password),
    method: asString(
      s.method,
      '2022-blake3-aes-128-gcm',
    ) as ShadowsocksOutboundFormSettings['method'],
    uot: asBool(s.uot),
    UoTVersion: asNumber(s.UoTVersion, 1),
  };
}

interface SimpleAuthFormSettings {
  address: string;
  port: number;
  user: string;
  pass: string;
}

function simpleAuthFromWire(raw: Raw, defaultPort: number): SimpleAuthFormSettings {
  const s = asObject(asArray(raw.servers)[0]);
  const u = asObject(asArray(s.users)[0]);
  return {
    address: asString(s.address),
    port: asPort(s.port, defaultPort),
    user: asString(u.user),
    pass: asString(u.pass),
  };
}

function stringRecordFromWire(raw: unknown): Record<string, string> {
  const obj = asObject(raw);
  const out: Record<string, string> = {};
  for (const [k, v] of Object.entries(obj)) {
    if (typeof v === 'string') out[k] = v;
  }
  return out;
}

// HTTP outbound reuses the SOCKS server/user shape but also carries xray's
// top-level `settings.headers` (HTTPClientConfig.Headers), the CONNECT
// headers sent to the upstream proxy. xray ignores per-server `headers`,
// so only the settings-level map round-trips (issue #5519).
function httpFromWire(raw: Raw): HttpOutboundFormSettings {
  return {
    ...simpleAuthFromWire(raw, 8080),
    headers: stringRecordFromWire(raw.headers),
  };
}

function wireguardFromWire(raw: Raw): WireguardOutboundFormSettings {
  const secretKey = asString(raw.secretKey);
  const pubKey = secretKey.length > 0 ? Wireguard.generateKeypair(secretKey).publicKey : '';
  const addressArr = asArray(raw.address).map((x) =>
    typeof x === 'number' ? String(x) : asString(x),
  );
  const reservedArr = asArray(raw.reserved).map((x) =>
    typeof x === 'number' ? String(x) : asString(x),
  );
  const peers: WireguardOutboundFormPeer[] = asArray(raw.peers).map((p) => {
    const pp = asObject(p);
    const allowed = asArray(pp.allowedIPs).map((x) => asString(x));
    return {
      publicKey: asString(pp.publicKey),
      psk: asString(pp.preSharedKey),
      allowedIPs: allowed.length > 0 ? allowed : ['0.0.0.0/0', '::/0'],
      endpoint: asString(pp.endpoint),
      keepAlive: asNumber(pp.keepAlive, 0),
    };
  });
  return {
    mtu: asNumber(raw.mtu, 1420),
    secretKey,
    pubKey,
    address: addressArr.join(','),
    domainStrategy: ((): WireguardOutboundFormSettings['domainStrategy'] => {
      const allowed = ['ForceIP', 'ForceIPv4', 'ForceIPv4v6', 'ForceIPv6', 'ForceIPv6v4'];
      const s = asString(raw.domainStrategy);
      return (allowed.includes(s) ? s : '') as WireguardOutboundFormSettings['domainStrategy'];
    })(),
    reserved: reservedArr.join(','),
    remoteDNS: asArray(raw.remoteDNS)
      .map((x) => asString(x))
      .join(','),
    peers,
    noKernelTun: asBool(raw.noKernelTun),
  };
}

function hysteriaFromWire(raw: Raw): HysteriaOutboundFormSettings {
  return {
    address: asString(raw.address),
    port: asPort(raw.port, 443),
    version: 2,
  };
}

function freedomFromWire(raw: Raw): FreedomOutboundFormSettings {
  const fragment = asObject(raw.fragment);
  const noises = asArray(raw.noises).map((n) => {
    const nn = asObject(n);
    return {
      type: asString(nn.type, 'rand') as FreedomOutboundFormSettings['noises'][number]['type'],
      packet: asString(nn.packet, '10-20'),
      delay: asString(nn.delay, '10-16'),
      applyTo: asString(
        nn.applyTo,
        'ip',
      ) as FreedomOutboundFormSettings['noises'][number]['applyTo'],
    };
  });
  const finalRulesRaw = asArray(raw.finalRules);
  const finalRules: FreedomFinalRuleForm[] = finalRulesRaw.map((r) => {
    const rr = asObject(r);
    const network = Array.isArray(rr.network)
      ? rr.network.map((x) => asString(x)).join(',')
      : asString(rr.network);
    return {
      action: (asString(rr.action, 'block') === 'allow'
        ? 'allow'
        : 'block') as FreedomFinalRuleForm['action'],
      network,
      port: asString(rr.port),
      ip: asArray(rr.ip).map((x) => asString(x)),
      blockDelay: asString(rr.blockDelay),
    };
  });
  // Legacy ipsBlocked → finalRule(block) backfill
  if (finalRules.length === 0) {
    const ipsBlocked = asArray(raw.ipsBlocked).map((x) => asString(x));
    if (ipsBlocked.length > 0) {
      finalRules.push({ action: 'block', network: '', port: '', ip: ipsBlocked, blockDelay: '' });
    }
  }
  // Wire fragment is either missing or a populated object. Mirror the
  // legacy behavior: when the wire omits fragment, leave all four fields
  // empty so the modal's "Fragment" Switch starts off. When present,
  // surface whatever the wire holds verbatim.
  const wireHasFragment =
    raw.fragment != null && typeof raw.fragment === 'object' && Object.keys(fragment).length > 0;
  return {
    domainStrategy: targetStrategyFromWire(
      asString(raw.targetStrategy) || asString(raw.domainStrategy),
    ),
    redirect: asString(raw.redirect),
    userLevel: asNumber(raw.userLevel, 0),
    proxyProtocol: ((): FreedomOutboundFormSettings['proxyProtocol'] => {
      const n = asNumber(raw.proxyProtocol, 0);
      return n === 1 || n === 2 ? n : 0;
    })(),
    fragment: wireHasFragment
      ? {
          packets: asString(fragment.packets, '1-3'),
          length: asString(fragment.length),
          interval: asString(fragment.interval),
          maxSplit: asString(fragment.maxSplit),
        }
      : { packets: '', length: '', interval: '', maxSplit: '' },
    noises,
    finalRules,
  };
}

function blackholeFromWire(raw: Raw): BlackholeOutboundFormSettings {
  const response = asObject(raw.response);
  const t = asString(response.type);
  return {
    type: t === 'none' || t === 'http' || t === 'custom' ? t : '',
    customResponseData: asString(response.customResponseData),
  };
}

function dnsRuleFromWire(raw: unknown): DnsRuleForm {
  const r = asObject(raw);
  const rawQType = r.qType ?? r.qtype;
  const qType = Array.isArray(rawQType)
    ? rawQType.map((x) => String(x)).join(',')
    : typeof rawQType === 'number'
      ? String(rawQType)
      : asString(rawQType);
  const domain = Array.isArray(r.domain)
    ? r.domain.map((x) => asString(x)).join(',')
    : asString(r.domain);
  const action = asString(r.action, 'direct');
  const validAction = ['direct', 'drop', 'return', 'hijack'].includes(action) ? action : 'direct';
  return {
    action: validAction as DnsRuleForm['action'],
    qType,
    domain,
    rCode: asNumber(r.rCode, 0),
  };
}

function dnsFromWire(raw: Raw): DnsOutboundFormSettings {
  const rules = asArray(raw.rules).map(dnsRuleFromWire);
  return {
    rewriteNetwork: ((): DnsOutboundFormSettings['rewriteNetwork'] => {
      const s = asString(raw.rewriteNetwork ?? raw.network);
      return s === 'udp' || s === 'tcp' ? s : '';
    })(),
    rewriteAddress: asString(raw.rewriteAddress ?? raw.address),
    rewritePort: asPort(raw.rewritePort ?? raw.port, 53),
    userLevel: asNumber(raw.userLevel, 0),
    rules,
  };
}

function loopbackFromWire(raw: Raw): LoopbackOutboundFormSettings {
  return {
    inboundTag: asString(raw.inboundTag),
    sniffing: sniffingFromWire(raw.sniffing),
  };
}

function muxFromWire(raw: unknown): MuxForm {
  const m = asObject(raw);
  return {
    enabled: asBool(m.enabled),
    concurrency: asNumber(m.concurrency, 8),
    xudpConcurrency: asNumber(m.xudpConcurrency, 16),
    xudpProxyUDP443: ((): MuxForm['xudpProxyUDP443'] => {
      const s = asString(m.xudpProxyUDP443, 'reject');
      return (['reject', 'allow', 'skip'].includes(s) ? s : 'reject') as MuxForm['xudpProxyUDP443'];
    })(),
  };
}

export interface RawOutboundRow {
  tag?: string;
  protocol?: string;
  sendThrough?: string;
  targetStrategy?: string;
  settings?: unknown;
  streamSettings?: unknown;
  mux?: unknown;
}

const XMUX_DEFAULTS = XHttpXmuxSchema.parse({});

function hydrateStreamForm(stream: Raw): OutboundStreamFormValues {
  const next = { ...stream };
  if (typeof next.method === 'string' && next.method !== '') {
    next.network = next.method;
  }
  delete next.method;
  const xh = next.xhttpSettings;
  if (xh && typeof xh === 'object' && !Array.isArray(xh)) {
    const xhttp = { ...(xh as Raw) };
    const xmux = xhttp.xmux;
    if (xmux && typeof xmux === 'object' && !Array.isArray(xmux)) {
      xhttp.enableXmux = true;
      xhttp.xmux = { ...XMUX_DEFAULTS, ...(xmux as Raw) };
    }
    next.xhttpSettings = xhttp;
  }
  return next as unknown as OutboundStreamFormValues;
}

export function rawOutboundToFormValues(raw: RawOutboundRow): OutboundFormValues {
  const protocol = asString(raw.protocol, 'vless');
  const settings = asObject(raw.settings);
  const tag = asString(raw.tag);
  const sendThrough = asString(raw.sendThrough);
  const targetStrategy = targetStrategyFromWire(raw.targetStrategy);
  const mux = muxFromWire(raw.mux);
  const hasStream =
    raw.streamSettings &&
    typeof raw.streamSettings === 'object' &&
    Object.keys(raw.streamSettings as Raw).length > 0;
  const streamSettings = hasStream ? hydrateStreamForm(raw.streamSettings as Raw) : undefined;

  let typed: OutboundFormSettings;
  switch (protocol) {
    case 'vmess':
      typed = { protocol: 'vmess', settings: vmessFromWire(settings) };
      break;
    case 'vless':
      typed = { protocol: 'vless', settings: vlessFromWire(settings) };
      break;
    case 'trojan':
      typed = { protocol: 'trojan', settings: trojanFromWire(settings) };
      break;
    case 'shadowsocks':
      typed = { protocol: 'shadowsocks', settings: shadowsocksFromWire(settings) };
      break;
    case 'socks':
      typed = { protocol: 'socks', settings: simpleAuthFromWire(settings, 1080) };
      break;
    case 'http':
      typed = { protocol: 'http', settings: httpFromWire(settings) };
      break;
    case 'wireguard':
      typed = { protocol: 'wireguard', settings: wireguardFromWire(settings) };
      break;
    case 'hysteria':
      typed = { protocol: 'hysteria', settings: hysteriaFromWire(settings) };
      break;
    case 'freedom':
      typed = { protocol: 'freedom', settings: freedomFromWire(settings) };
      break;
    case 'blackhole':
      typed = { protocol: 'blackhole', settings: blackholeFromWire(settings) };
      break;
    case 'dns':
      typed = { protocol: 'dns', settings: dnsFromWire(settings) };
      break;
    case 'loopback':
      typed = { protocol: 'loopback', settings: loopbackFromWire(settings) };
      break;
    default:
      typed = { protocol: 'vless', settings: vlessFromWire(settings) };
  }

  return {
    ...typed,
    tag,
    sendThrough,
    targetStrategy,
    mux,
    streamSettings,
  };
}

// --- Form values -> wire payload --------------------------------------

function vmessToWire(s: VmessOutboundFormSettings) {
  return {
    vnext: [
      {
        address: s.address,
        port: s.port,
        users: [{ id: s.id, security: s.security }],
      },
    ],
  };
}

function sniffingToWire(s: Sniffing) {
  return {
    enabled: s.enabled,
    destOverride: s.destOverride,
    metadataOnly: s.metadataOnly,
    routeOnly: s.routeOnly,
    ipsExcluded: s.ipsExcluded.length > 0 ? s.ipsExcluded : undefined,
    domainsExcluded: s.domainsExcluded.length > 0 ? s.domainsExcluded : undefined,
  };
}

function vlessToWire(s: VlessOutboundFormSettings) {
  const result: Raw = {
    address: s.address,
    port: s.port,
    id: s.id,
    flow: s.flow,
    encryption: s.encryption || 'none',
  };
  if (s.reverseTag) {
    const sn = sniffingToWire(s.reverseSniffing);
    const defaultSn = sniffingToWire(SNIFFING_DEFAULT);
    result.reverse = {
      tag: s.reverseTag,
      sniffing: JSON.stringify(sn) === JSON.stringify(defaultSn) ? {} : sn,
    };
  }
  if (s.flow === 'xtls-rprx-vision') {
    if (s.testpre > 0) result.testpre = s.testpre;
    if (s.testseed.length === 4 && s.testseed.every((v) => Number.isInteger(v) && v > 0)) {
      result.testseed = s.testseed;
    }
  }
  return result;
}

function trojanToWire(s: TrojanOutboundFormSettings) {
  return { servers: [{ address: s.address, port: s.port, password: s.password }] };
}

function shadowsocksToWire(s: ShadowsocksOutboundFormSettings) {
  return {
    servers: [
      {
        address: s.address,
        port: s.port,
        password: s.password,
        method: s.method,
        uot: s.uot,
        UoTVersion: s.UoTVersion,
      },
    ],
  };
}

function simpleAuthToWire(s: SimpleAuthFormSettings) {
  return {
    servers: [
      {
        address: s.address,
        port: s.port,
        users: s.user ? [{ user: s.user, pass: s.pass }] : [],
      },
    ],
  };
}

function httpToWire(s: HttpOutboundFormSettings): Raw {
  const wire: Raw = simpleAuthToWire(s);
  if (s.headers && Object.keys(s.headers).length > 0) {
    wire.headers = s.headers;
  }
  return wire;
}

function wireguardToWire(s: WireguardOutboundFormSettings) {
  return {
    mtu: s.mtu || undefined,
    secretKey: s.secretKey,
    address: s.address
      ? s.address
          .split(',')
          .map((x) => x.trim())
          .filter(Boolean)
      : [],
    domainStrategy: s.domainStrategy || undefined,
    reserved: s.reserved
      ? s.reserved
          .split(',')
          .map((x) => Number(x.trim()))
          .filter((n) => Number.isFinite(n))
      : undefined,
    remoteDNS: s.remoteDNS
      ? s.remoteDNS
          .split(',')
          .map((x) => x.trim())
          .filter(Boolean)
      : undefined,
    peers: s.peers.map((p) => ({
      publicKey: p.publicKey,
      preSharedKey: p.psk.length > 0 ? p.psk : undefined,
      allowedIPs: p.allowedIPs.length > 0 ? p.allowedIPs : undefined,
      endpoint: p.endpoint,
      keepAlive: p.keepAlive || undefined,
    })),
    noKernelTun: s.noKernelTun,
  };
}

function hysteriaToWire(s: HysteriaOutboundFormSettings) {
  return { address: s.address, port: s.port, version: s.version };
}

function freedomToWire(s: FreedomOutboundFormSettings) {
  // The strategy is emitted under the legacy domainStrategy key: new cores
  // fall back to it when targetStrategy is absent, old cores only know it.
  // Legacy semantics: emit fragment only when the user actually populated
  // at least one of the four sub-fields. Defaults like packets='1-3' alone
  // are not enough — the modal's Fragment Switch sets all four together.
  // getFieldsValue(true) may omit `fragment` when the switch is off, so the
  // fallback keeps Object.entries from throwing on undefined (issue #4686).
  const fragment: Partial<FreedomOutboundFormSettings['fragment']> = s.fragment ?? {};
  const fragmentEntries = Object.entries(fragment).filter(([, v]) => v !== '' && v != null);
  const fragmentEnabled = !!fragment.length || !!fragment.interval || !!fragment.maxSplit;
  return {
    domainStrategy: s.domainStrategy || undefined,
    redirect: s.redirect || undefined,
    userLevel: s.userLevel || undefined,
    proxyProtocol: s.proxyProtocol || undefined,
    fragment: fragmentEnabled ? Object.fromEntries(fragmentEntries) : undefined,
    noises: s.noises && s.noises.length > 0 ? s.noises : undefined,
    finalRules:
      s.finalRules && s.finalRules.length > 0
        ? s.finalRules.map((r) => ({
            action: r.action,
            network: r.network || undefined,
            port: r.port || undefined,
            ip: r.ip.length > 0 ? r.ip : undefined,
            blockDelay: r.action === 'block' && r.blockDelay ? r.blockDelay : undefined,
          }))
        : undefined,
  };
}

function blackholeToWire(s: BlackholeOutboundFormSettings) {
  if (!s.type) return { response: undefined };
  if (s.type === 'custom') {
    return { response: { type: s.type, customResponseData: s.customResponseData } };
  }
  return { response: { type: s.type } };
}

function dnsRuleToWire(r: DnsRuleForm) {
  const action = ['direct', 'drop', 'return', 'hijack'].includes(r.action) ? r.action : 'direct';
  const result: Raw = { action };
  const qType = r.qType.trim();
  if (qType) {
    result.qType = /^\d+$/.test(qType) ? Number(qType) : qType;
  }
  const domains = r.domain
    .split(',')
    .map((d) => d.trim())
    .filter(Boolean);
  if (domains.length > 0) result.domain = domains;
  if (r.rCode > 0) result.rCode = r.rCode;
  return result;
}

function dnsToWire(s: DnsOutboundFormSettings) {
  const result: Raw = {};
  if (s.rewriteNetwork) result.rewriteNetwork = s.rewriteNetwork;
  if (s.rewriteAddress) result.rewriteAddress = s.rewriteAddress;
  if (s.rewritePort) result.rewritePort = s.rewritePort;
  if (s.userLevel) result.userLevel = s.userLevel;
  if (s.rules.length > 0) result.rules = s.rules.map(dnsRuleToWire);
  return result;
}

function loopbackToWire(s: LoopbackOutboundFormSettings) {
  const result: Raw = { inboundTag: s.inboundTag || undefined };
  // Sniffing rides only when enabled — a disabled block is a no-op for
  // xray's BuildSniffingRequest, so omitting it keeps the wire minimal.
  if (s.sniffing.enabled) {
    result.sniffing = sniffingToWire(s.sniffing);
  }
  return result;
}

// canEnableMux mirrors the legacy Outbound.canEnableMux().
const MUX_PROTOCOLS = new Set(['vmess', 'vless', 'trojan', 'shadowsocks', 'http', 'socks']);
const STREAM_PROTOCOLS = new Set(['vmess', 'vless', 'trojan', 'shadowsocks', 'hysteria']);

function dropEmptyStrings(obj: Raw): Raw {
  const out: Raw = {};
  for (const [k, v] of Object.entries(obj)) {
    if (v === '') continue;
    out[k] = v;
  }
  return out;
}

function stripUiOnlyStreamFields(stream: unknown): Raw {
  const next = { ...(stream as Raw) };
  const xh = next.xhttpSettings;
  if (xh && typeof xh === 'object') {
    const cleaned = { ...(xh as Raw) };
    const xmuxEnabled = cleaned.enableXmux === true;
    delete cleaned.enableXmux;
    if (!xmuxEnabled) delete cleaned.xmux;
    next.xhttpSettings = dropEmptyStrings(cleaned);
  }
  return normalizeStreamSettingsForWire(next, { side: 'outbound' }) as Raw;
}

function muxAllowed(values: OutboundFormValues): boolean {
  if (!MUX_PROTOCOLS.has(values.protocol)) return false;
  const flow =
    values.protocol === 'vless' ? (values.settings as VlessOutboundFormSettings).flow : '';
  if (flow) return false;
  const network =
    values.streamSettings && 'network' in values.streamSettings
      ? values.streamSettings.network
      : undefined;
  if (network === 'xhttp') return false;
  return true;
}

export type WireOutboundPayload = Raw;

export function formValuesToWirePayload(values: OutboundFormValues): WireOutboundPayload {
  let settings: Raw;
  switch (values.protocol) {
    case 'vmess':
      settings = vmessToWire(values.settings);
      break;
    case 'vless':
      settings = vlessToWire(values.settings);
      break;
    case 'trojan':
      settings = trojanToWire(values.settings);
      break;
    case 'shadowsocks':
      settings = shadowsocksToWire(values.settings);
      break;
    case 'socks':
      settings = simpleAuthToWire(values.settings);
      break;
    case 'http':
      settings = httpToWire(values.settings);
      break;
    case 'wireguard':
      settings = wireguardToWire(values.settings);
      break;
    case 'hysteria':
      settings = hysteriaToWire(values.settings);
      break;
    case 'freedom':
      settings = freedomToWire(values.settings);
      break;
    case 'blackhole':
      settings = blackholeToWire(values.settings);
      break;
    case 'dns':
      settings = dnsToWire(values.settings);
      break;
    case 'loopback':
      settings = loopbackToWire(values.settings);
      break;
  }

  const result: Raw = {
    protocol: values.protocol,
    settings,
  };
  if (values.tag) result.tag = values.tag;
  if (values.targetStrategy) result.targetStrategy = values.targetStrategy;

  // streamSettings emission gates on canEnableStream — non-stream protocols
  // still emit just `sockopt` if that key is present (legacy behavior).
  if (values.streamSettings) {
    if (STREAM_PROTOCOLS.has(values.protocol)) {
      result.streamSettings = stripUiOnlyStreamFields(values.streamSettings);
    } else {
      const sockopt = (values.streamSettings as { sockopt?: unknown }).sockopt;
      if (sockopt) result.streamSettings = { sockopt };
    }
  }

  if (values.sendThrough) result.sendThrough = values.sendThrough;
  // mux may be absent when the modal didn't render the Mux switch (non-
  // stream protocols or when isMuxAllowed gated it out). validateFields()
  // only returns registered fields, so values.mux can be undefined.
  if (values.mux?.enabled && muxAllowed(values)) {
    result.mux = values.mux;
  }
  return result;
}
