import { Host, TerminalSettings } from './models';
import { migrateDeprecatedFontOverride } from '../infrastructure/config/fonts';

export type HostLabelRenameResult =
  | { ok: true; changed: true; hosts: Host[] }
  | { ok: true; changed: false; reason: 'unchanged' | 'missing'; hosts: Host[] }
  | { ok: false; changed: false; reason: 'required'; hosts: Host[] };

export function applyHostLabelRename(
  hosts: Host[],
  hostId: string,
  rawLabel: string,
): HostLabelRenameResult {
  const nextLabel = rawLabel.trim();
  if (!nextLabel) {
    return { ok: false, changed: false, reason: 'required', hosts };
  }

  let found = false;
  let changed = false;
  const nextHosts = hosts.map((host) => {
    if (host.id !== hostId) return host;
    found = true;
    if (host.label === nextLabel) return host;
    changed = true;
    return { ...host, label: nextLabel };
  });

  if (!found) return { ok: true, changed: false, reason: 'missing', hosts };
  if (!changed) return { ok: true, changed: false, reason: 'unchanged', hosts };
  return { ok: true, changed: true, hosts: nextHosts };
}

export const LINUX_DISTRO_OPTIONS = [
  'linux',
  'ubuntu',
  'debian',
  'centos',
  'rocky',
  'fedora',
  'arch',
  'alpine',
  'amazon',
  'opensuse',
  'redhat',
  'almalinux',
  'oracle',
  'kali',
  'alinux',
] as const;

/**
 * Known network-device vendor IDs that Netcatty can detect from the SSH
 * server identification string. When a host is classified as one of these,
 * features that assume a POSIX shell (e.g. the periodic server stats poll)
 * are disabled, because the stats command would either be rejected outright
 * or generate one AAA session log per poll on the remote device.
 */
export const NETWORK_DEVICE_OPTIONS = [
  'cisco',
  'juniper',
  'huawei',
  'hpe',
  'mikrotik',
  'fortinet',
  'paloalto',
  'zyxel',
  'ruijie',
] as const;

export type NetworkDeviceVendor = typeof NETWORK_DEVICE_OPTIONS[number];

export const normalizeDistroId = (value?: string) => {
  const v = (value || '').toLowerCase().trim();
  if (!v) return '';
  if (v.includes('ubuntu')) return 'ubuntu';
  if (v.includes('debian')) return 'debian';
  if (v.includes('centos')) return 'centos';
  if (v.includes('rocky')) return 'rocky';
  if (v.includes('fedora')) return 'fedora';
  if (v.includes('arch') || v.includes('manjaro')) return 'arch';
  if (v.includes('alpine')) return 'alpine';
  if (v.includes('amzn') || v.includes('amazon') || v.includes('aws')) return 'amazon';
  if (v.includes('opensuse') || v.includes('suse') || v.includes('sles')) return 'opensuse';
  if (v.includes('red hat') || v.includes('redhat') || v.includes('rhel')) return 'redhat';
  if (v.includes('almalinux')) return 'almalinux';
  if (v.includes('oracle')) return 'oracle';
  if (v.includes('kali')) return 'kali';
  // Alibaba Cloud Linux: os-release ID is `alinux` (older branding: Aliyun
  // Linux / `aliyun`). Must come before the generic `linux` fallback because
  // 'alinux'.includes('linux') is true and would otherwise resolve to 'linux'.
  if (v.includes('alinux') || v.includes('aliyun') || v.includes('alibaba cloud')) {
    return 'alinux';
  }
  // Network device vendor IDs may arrive here after detection — preserve them.
  if ((NETWORK_DEVICE_OPTIONS as readonly string[]).includes(v)) return v;
  if (v === 'linux' || v.includes('linux')) return 'linux';
  return '';
};

/**
 * Parse the SSH server identification string (the `software` portion of
 * `SSH-<protocol>-<software>\r\n`, exposed by ssh2 as `conn._remoteVer`)
 * and return a normalized network-device vendor ID, or '' if the ident
 * does not match a known vendor.
 *
 * Matching patterns are sourced from the Nmap nmap-service-probes
 * database (match lines beginning with `match ssh m|^SSH-`), cross-
 * referenced with the ssh-audit project's software.py and vendor docs.
 * Only rules whose pattern can be reproduced from those sources are
 * included here.
 *
 * Empty string means "use the fallback linux/macos detection path" —
 * that is what happens for OpenSSH, Dropbear, JUNOS, Cisco NX-OS, and
 * Arista EOS, all of which either are POSIX systems or present as
 * plain `OpenSSH_*` with no distinct vendor marker.
 */
export const detectVendorFromSshVersion = (softwareVersion?: string): '' | NetworkDeviceVendor => {
  const s = (softwareVersion || '').trim().replace(/^SSH-(?:2\.0|1\.99)-/i, '');
  if (!s) return '';

  // Cisco family — IOS, IOS XA, Wireless LAN Controller
  if (/^Cisco[-_]/i.test(s)) return 'cisco';
  if (/^CiscoIOS_/i.test(s)) return 'cisco';
  if (/^CISCO_WLC\b/.test(s)) return 'cisco';
  // Note: `IPSSH-*` is used by both Cisco and 3Com devices (per Nmap
  // `match ssh m|^SSH-([\d.]+)-IPSSH-([\d.]+)\|`), so we cannot map it
  // to a specific vendor icon from the banner alone. Users who want a
  // custom icon for such devices can set one via the Host Details
  // manual distro override. The stats-polling gate is still handled
  // correctly via `host.deviceType === 'network'`.

  // Juniper NetScreen firewall (JUNOS itself uses OpenSSH and is caught
  // by the fallback failure-counter path in useServerStats).
  if (/^NetScreen\b/.test(s)) return 'juniper';

  // Huawei VRP and related products
  if (s === '-') return 'huawei';
  if (/^HUAWEI[-_]/i.test(s)) return 'huawei';
  if (/^VRP-/i.test(s)) return 'huawei';

  // HPE / H3C — Comware switches, Integrated Lights-Out (iLO), legacy 3Com
  if (/^Comware-/i.test(s)) return 'hpe';
  if (/^3Com\s*OS/i.test(s)) return 'hpe';
  if (/^mpSSH_/i.test(s)) return 'hpe';

  // MikroTik RouterOS
  if (/^ROSSSH\b/.test(s)) return 'mikrotik';

  // Fortinet FortiOS / FortiGate
  if (/^FortiSSH_/i.test(s)) return 'fortinet';

  // Palo Alto Networks PAN-OS
  if (/^PaloAltoNetworks[_-]/i.test(s)) return 'paloalto';

  // ZyXEL ZyWALL
  if (/^Zyxel\s*SSH/i.test(s)) return 'zyxel';

  // Ruijie RGOS
  if (/^RGOS_SSH\b/i.test(s)) return 'ruijie';

  return '';
};

/**
 * Classify a distro/vendor ID into a high-level device class. Features that
 * assume a POSIX shell (periodic stats polling, /etc/os-release probing, etc.)
 * should only run when this returns `linux-like`.
 */
export type DeviceClass = 'linux-like' | 'network-device' | 'other';

export const classifyDistroId = (distroId?: string): DeviceClass => {
  const v = (distroId || '').toLowerCase().trim();
  if (!v) return 'other';
  if ((NETWORK_DEVICE_OPTIONS as readonly string[]).includes(v)) return 'network-device';
  if ((LINUX_DISTRO_OPTIONS as readonly string[]).includes(v)) return 'linux-like';
  return 'other';
};

/**
 * Decide whether it is safe to run the post-connect `pwd` probe that
 * discovers the session's working directory. The probe opens an extra exec
 * channel running a POSIX-shell script; strict network-device CLIs such as
 * Huawei VRP respond by closing the whole SSH session (#1043), so it must be
 * skipped for them.
 *
 * `isNetworkDevice` covers hosts we already classified (a reconnect, or an
 * explicit `deviceType: 'network'`). On a brand-new host that field is not
 * populated yet, so we also inspect the SSH server identification banner —
 * captured for free at handshake — which identifies most vendors directly.
 */
export const shouldProbeSessionCwd = (opts: {
  isNetworkDevice: boolean;
  remoteSshVersion?: string;
}): boolean =>
  !opts.isNetworkDevice && !detectVendorFromSshVersion(opts.remoteSshVersion);

export const getEffectiveHostDistro = (
  host?: Pick<Host, 'distro' | 'manualDistro' | 'distroMode'> | null,
) => {
  if (!host) return '';
  const detected = normalizeDistroId(host.distro);
  const manual = normalizeDistroId(host.manualDistro);
  if (host.distroMode === 'manual') return manual || detected;
  if (host.distroMode === 'auto') return detected;
  return detected;
};

/** Format hostname:port for display, wrapping IPv6 addresses in brackets. */
export const formatHostPort = (hostname: string, port?: number | null): string => {
  if (port == null) return hostname;
  const isIPv6 = hostname.includes(':') && !hostname.startsWith('[');
  const display = isIPv6 ? `[${hostname}]` : hostname;
  return `${display}:${port}`;
};

export const resolveTelnetUsername = (
  host: Pick<Host, 'telnetUsername' | 'username'>,
): string | undefined =>
  host.telnetUsername !== undefined
    ? host.telnetUsername.trim()
    : host.username?.trim();

export const resolveTelnetPassword = (
  host: Pick<Host, 'telnetPassword' | 'password'>,
): string | undefined =>
  host.telnetPassword !== undefined
    ? host.telnetPassword
    : host.password;

export const resolveTelnetPort = (
  host: Pick<Host, 'protocol' | 'telnetPort' | 'port'>,
): number => {
  if (host.telnetPort !== undefined && host.telnetPort !== null) return host.telnetPort;
  if (host.protocol === 'telnet' && host.port !== undefined && host.port !== null) {
    return host.port;
  }
  return 23;
};

export const normalizePrimaryTelnetState = (host: Host): Host =>
  host.protocol === 'telnet' && !host.telnetEnabled
    ? { ...host, telnetEnabled: true }
    : host;

export const migrateHostsFromLegacyLineTimestamps = (
  hosts: Host[],
  legacyEnabled: boolean,
): Host[] => {
  if (!legacyEnabled) return hosts;
  let changed = false;
  const migrated = hosts.map((host) => {
    if (host.showLineTimestamps !== undefined) return host;
    changed = true;
    return { ...host, showLineTimestamps: true };
  });
  return changed ? migrated : hosts;
};

export const preserveConcurrentHostLineTimestampUpdate = ({
  draft,
  openedHost,
  latestHost,
}: {
  draft: Host;
  openedHost?: Host | null;
  latestHost?: Host | null;
}): Host => {
  if (!openedHost || !latestHost) return draft;
  if (draft.id !== openedHost.id || draft.id !== latestHost.id) return draft;
  if (draft.showLineTimestamps !== openedHost.showLineTimestamps) return draft;
  if (latestHost.showLineTimestamps === openedHost.showLineTimestamps) return draft;
  return { ...draft, showLineTimestamps: latestHost.showLineTimestamps };
};

export const upsertHostById = (hosts: Host[], host: Host): Host[] => {
  const hostExists = hosts.some((entry) => entry.id === host.id);
  return hostExists
    ? hosts.map((entry) => (entry.id === host.id ? host : entry))
    : [...hosts, host];
};

export interface ResolvedKeepalive {
  interval: number; // Seconds; 0 = disabled
  countMax: number; // Unanswered keepalives before declaring dead
  source: 'host' | 'global';
}

/**
 * Decide which SSH keepalive values to apply to a connection. A host can opt
 * into its own values via `keepaliveOverride === true` — useful when a
 * specific device (older router / switch / NOKIA / ALCATEL SSH stack) doesn't
 * reply to keepalive@openssh.com and the global aggressive setting would
 * cause the session to be declared dead after a handful of unanswered probes.
 * When the override is off (the default), the host inherits the global
 * TerminalSettings values which are tuned for cloud / NAT'd hosts.
 *
 * Each field falls back independently: a host can override only the interval
 * while still inheriting the global countMax, and vice versa.
 */
export const resolveHostKeepalive = (
  host: Pick<Host, 'keepaliveOverride' | 'keepaliveInterval' | 'keepaliveCountMax'>,
  globalSettings: Pick<TerminalSettings, 'keepaliveInterval' | 'keepaliveCountMax'>,
): ResolvedKeepalive => {
  const globalInterval = globalSettings.keepaliveInterval;
  const globalCountMax = globalSettings.keepaliveCountMax;
  if (host.keepaliveOverride !== true) {
    return { interval: globalInterval, countMax: globalCountMax, source: 'global' };
  }
  return {
    interval: host.keepaliveInterval ?? globalInterval,
    countMax: host.keepaliveCountMax ?? globalCountMax,
    source: 'host',
  };
};

export const sanitizeHost = (host: Host): Host => {
  const cleanHostname = (host.hostname || '').split(/\s+/)[0];
  const cleanDistro = normalizeDistroId(host.distro);
  const cleanManualDistro = normalizeDistroId(host.manualDistro);
  const cleanDistroMode =
    host.distroMode === 'manual'
      ? 'manual'
      : host.distroMode === 'auto'
        ? 'auto'
        : undefined;
  const migrated = migrateDeprecatedFontOverride(host);
  const cleanNotes = host.notes?.trim() || undefined;
  return {
    ...migrated,
    hostname: cleanHostname,
    distro: cleanDistro,
    distroMode: cleanDistroMode,
    manualDistro: cleanManualDistro || undefined,
    notes: cleanNotes,
  };
};
