import * as plugins from './smartnetwork.plugins.js';
import { CloudflareSpeed } from './smartnetwork.classes.cloudflarespeed.js';
import { PublicIp } from './smartnetwork.classes.publicip.js';
import { IpIntelligence, type IIpIntelligenceResult } from './smartnetwork.classes.ipintelligence.js';
import { getLogger } from './logging.js';
import { NetworkError } from './errors.js';
import { RustNetworkBridge } from './smartnetwork.classes.rustbridge.js';

/**
 * Configuration options for SmartNetwork
 */
export interface SmartNetworkOptions {
  /** Cache time-to-live in milliseconds for gateway and public IP lookups */
  cacheTtl?: number;
}

/**
 * A hop in a traceroute result
 */
export interface Hop {
  ttl: number;
  ip: string;
  rtt: number | null;
}

/**
 * Options for the findFreePort method
 */
export interface IFindFreePortOptions {
  /** If true, selects a random available port within the range instead of the first one */
  randomize?: boolean;
  /** Array of port numbers to exclude from the search */
  exclude?: number[];
}

/**
 * SmartNetwork simplifies actions within the network.
 * Uses a Rust binary for system-dependent operations (ping, traceroute, port scanning, gateway detection).
 */
export class SmartNetwork {
  /** Static registry for external plugins */
  public static pluginsRegistry: Map<string, any> = new Map();
  /** Register a plugin by name */
  public static registerPlugin(name: string, ctor: any): void {
    SmartNetwork.pluginsRegistry.set(name, ctor);
  }
  /** Unregister a plugin by name */
  public static unregisterPlugin(name: string): void {
    SmartNetwork.pluginsRegistry.delete(name);
  }

  private options: SmartNetworkOptions;
  private cache: Map<string, { value: any; expiry: number }>;
  private rustBridge: RustNetworkBridge;
  private bridgeStarted = false;
  private ipIntelligence: IpIntelligence | null = null;

  constructor(options?: SmartNetworkOptions) {
    this.options = options || {};
    this.cache = new Map();
    this.rustBridge = RustNetworkBridge.getInstance();
  }

  /**
   * Start the Rust binary bridge. Must be called before using ping, traceroute,
   * port scanning, or gateway operations. Safe to call multiple times.
   */
  public async start(): Promise<void> {
    if (!this.bridgeStarted) {
      await this.rustBridge.start();
      this.bridgeStarted = true;
    }
  }

  /**
   * Stop the Rust binary bridge.
   */
  public async stop(): Promise<void> {
    if (this.bridgeStarted) {
      await this.rustBridge.stop();
      this.bridgeStarted = false;
    }
  }

  /**
   * Ensure the Rust bridge is running before sending commands.
   */
  private async ensureBridge(): Promise<void> {
    if (!this.bridgeStarted) {
      await this.start();
    }
  }

  /**
   * Get network speed via Cloudflare speed test (pure TS, no Rust needed).
   */
  public async getSpeed(opts?: { parallelStreams?: number; duration?: number }) {
    const cloudflareSpeedInstance = new CloudflareSpeed(opts);
    return cloudflareSpeedInstance.speedTest();
  }

  /**
   * Send ICMP pings to a host. Optionally specify count for multiple pings.
   */
  public async ping(host: string, opts?: { timeout?: number; count?: number }): Promise<any> {
    await this.ensureBridge();
    const timeoutMs = opts?.timeout ?? 5000;
    const count = opts?.count && opts.count > 1 ? opts.count : 1;

    let result: Awaited<ReturnType<typeof this.rustBridge.ping>>;
    try {
      result = await this.rustBridge.ping(host, count, timeoutMs);
    } catch {
      // DNS resolution failure or other error — return dead ping
      if (count === 1) {
        return { alive: false, time: NaN };
      }
      return {
        host,
        count,
        times: Array(count).fill(NaN),
        min: NaN,
        max: NaN,
        avg: NaN,
        stddev: NaN,
        packetLoss: 100,
        alive: false,
      };
    }

    // Map times: replace null with NaN for backward compatibility
    const times = result.times.map((t) => (t === null ? NaN : t));
    const min = result.min === null ? NaN : result.min;
    const max = result.max === null ? NaN : result.max;
    const avg = result.avg === null ? NaN : result.avg;
    const stddev = result.stddev === null ? NaN : result.stddev;

    if (count === 1) {
      return {
        alive: result.alive,
        time: times[0] ?? NaN,
      };
    }

    return {
      host,
      count,
      times,
      min,
      max,
      avg,
      stddev,
      packetLoss: result.packetLoss,
      alive: result.alive,
    };
  }

  /**
   * Check if a local port is unused (both IPv4 and IPv6)
   */
  public async isLocalPortUnused(port: number): Promise<boolean> {
    await this.ensureBridge();
    const result = await this.rustBridge.isLocalPortFree(port);
    return result.free;
  }

  /**
   * Find the first available port within a given range
   */
  public async findFreePort(startPort: number, endPort: number, options?: IFindFreePortOptions): Promise<number | null> {
    // Validate port range
    if (startPort < 1 || startPort > 65535 || endPort < 1 || endPort > 65535) {
      throw new NetworkError('Port numbers must be between 1 and 65535', 'EINVAL');
    }
    if (startPort > endPort) {
      throw new NetworkError('Start port must be less than or equal to end port', 'EINVAL');
    }

    const excludedPorts = new Set(options?.exclude || []);

    if (options?.randomize) {
      const availablePorts: number[] = [];
      for (let port = startPort; port <= endPort; port++) {
        if (excludedPorts.has(port)) continue;
        const isUnused = await this.isLocalPortUnused(port);
        if (isUnused) {
          availablePorts.push(port);
        }
      }
      if (availablePorts.length > 0) {
        const randomIndex = Math.floor(Math.random() * availablePorts.length);
        return availablePorts[randomIndex];
      }
      return null;
    } else {
      for (let port = startPort; port <= endPort; port++) {
        if (excludedPorts.has(port)) continue;
        const isUnused = await this.isLocalPortUnused(port);
        if (isUnused) {
          return port;
        }
      }
      return null;
    }
  }

  /**
   * Check if a remote port is available
   */
  public async isRemotePortAvailable(
    target: string,
    portOrOpts?:
      | number
      | { port?: number; protocol?: 'tcp' | 'udp'; timeout?: number; retries?: number },
  ): Promise<boolean> {
    let hostPart: string;
    let port: number | undefined;
    let protocol: string = 'tcp';
    let retries = 1;
    let timeout: number | undefined;

    if (typeof portOrOpts === 'number') {
      [hostPart] = target.split(':');
      port = portOrOpts;
    } else {
      const opts = portOrOpts || {};
      protocol = opts.protocol ?? 'tcp';
      retries = opts.retries ?? 1;
      timeout = opts.timeout;
      [hostPart] = target.split(':');
      const portPart = target.split(':')[1];
      port = opts.port ?? (portPart ? parseInt(portPart, 10) : undefined);
    }

    if (protocol === 'udp') {
      throw new NetworkError('UDP port check not supported', 'ENOTSUP');
    }
    if (!port) {
      throw new NetworkError('Port not specified', 'EINVAL');
    }

    await this.ensureBridge();
    let last = false;
    for (let attempt = 0; attempt < retries; attempt++) {
      try {
        const result = await this.rustBridge.tcpPortCheck(hostPart, port, timeout);
        last = result.isOpen;
        if (last) return true;
      } catch {
        // DNS resolution failure or connection error — treat as not available
        last = false;
      }
    }
    return last;
  }

  /**
   * List network interfaces (gateways) — pure TS, no Rust needed.
   */
  public async getGateways(): Promise<Record<string, plugins.os.NetworkInterfaceInfo[]>> {
    const fetcher = async () => plugins.os.networkInterfaces() as Record<string, plugins.os.NetworkInterfaceInfo[]>;
    if (this.options.cacheTtl && this.options.cacheTtl > 0) {
      return this.getCached('gateways', fetcher);
    }
    return fetcher();
  }

  /**
   * Get the default gateway interface and its addresses.
   */
  public async getDefaultGateway(): Promise<{
    ipv4: plugins.os.NetworkInterfaceInfo;
    ipv6: plugins.os.NetworkInterfaceInfo;
  } | null> {
    await this.ensureBridge();
    const result = await this.rustBridge.defaultGateway();
    const interfaceName = result.interfaceName;

    if (!interfaceName) {
      getLogger().warn?.('Cannot determine default gateway');
      return null;
    }

    // Use os.networkInterfaces() to get rich interface info
    const gateways = await this.getGateways();
    const defaultGateway = gateways[interfaceName];
    if (!defaultGateway) {
      getLogger().warn?.(`Interface ${interfaceName} not found in os.networkInterfaces()`);
      return null;
    }

    return {
      ipv4: defaultGateway[0],
      ipv6: defaultGateway[1],
    };
  }

  /**
   * Lookup public IPv4 and IPv6 — pure TS, no Rust needed.
   */
  public async getPublicIps(): Promise<{ v4: string | null; v6: string | null }> {
    const fetcher = async () => {
      const publicIp = new PublicIp({ timeout: 1000 });
      return publicIp.getPublicIps();
    };
    if (this.options.cacheTtl && this.options.cacheTtl > 0) {
      return this.getCached('publicIps', fetcher);
    }
    return fetcher();
  }

  /**
   * Resolve DNS records (A, AAAA, MX) — uses smartdns, no Rust needed.
   */
  public async resolveDns(
    host: string,
  ): Promise<{ A: string[]; AAAA: string[]; MX: { exchange: string; priority: number }[] }> {
    try {
      const dnsClient = new plugins.smartdns.dnsClientMod.Smartdns({
        strategy: 'prefer-system',
        allowDohFallback: true,
      });

      const [aRecords, aaaaRecords, mxRecords] = await Promise.all([
        dnsClient.getRecordsA(host).catch((): any[] => []),
        dnsClient.getRecordsAAAA(host).catch((): any[] => []),
        dnsClient.getRecords(host, 'MX').catch((): any[] => []),
      ]);

      const A = aRecords.map((record: any) => record.value);
      const AAAA = aaaaRecords.map((record: any) => record.value);
      const MX = mxRecords.map((record: any) => {
        const parts = record.value.split(' ');
        return {
          priority: parseInt(parts[0], 10),
          exchange: parts[1] || '',
        };
      });

      return { A, AAAA, MX };
    } catch (err: any) {
      throw new NetworkError(err.message, err.code);
    }
  }

  /**
   * Perform a simple HTTP/HTTPS endpoint health check — pure TS.
   */
  public async checkEndpoint(
    urlString: string,
    opts?: { timeout?: number; rejectUnauthorized?: boolean },
  ): Promise<{ status: number; headers: Record<string, string>; rtt: number }> {
    const start = plugins.perfHooks.performance.now();
    try {
      const url = new URL(urlString);
      const lib = url.protocol === 'https:' ? plugins.https : await import('node:http');
      return new Promise((resolve, reject) => {
        const req = lib.request(
          url,
          { method: 'GET', timeout: opts?.timeout, agent: false, rejectUnauthorized: opts?.rejectUnauthorized ?? true },
          (res: any) => {
            res.on('data', () => {});
            res.once('end', () => {
              const rtt = plugins.perfHooks.performance.now() - start;
              const headers: Record<string, string> = {};
              for (const [k, v] of Object.entries(res.headers)) {
                headers[k] = Array.isArray(v) ? v.join(',') : String(v);
              }
              resolve({ status: res.statusCode, headers, rtt });
            });
          },
        );
        req.on('error', (err: any) => reject(new NetworkError(err.message, err.code)));
        req.end();
      });
    } catch (err: any) {
      throw new NetworkError(err.message, err.code);
    }
  }

  /**
   * Perform a traceroute: hop-by-hop latency using the Rust binary.
   */
  public async traceroute(
    host: string,
    opts?: { maxHops?: number; timeout?: number },
  ): Promise<Hop[]> {
    await this.ensureBridge();
    const maxHops = opts?.maxHops ?? 30;
    const timeoutMs = opts?.timeout ?? 5000;

    const result = await this.rustBridge.traceroute(host, maxHops, timeoutMs);
    return result.hops.map((h) => ({
      ttl: h.ttl,
      ip: h.ip || '*',
      rtt: h.rtt,
    }));
  }

  /**
   * Get IP intelligence: ASN, organization, geolocation, and RDAP registration data.
   * Combines RDAP (RIRs), Team Cymru DNS, and MaxMind GeoLite2 — all run in parallel.
   * Pure TS, no Rust needed.
   */
  public async getIpIntelligence(ip: string): Promise<IIpIntelligenceResult> {
    if (!this.ipIntelligence) {
      this.ipIntelligence = new IpIntelligence();
    }
    const fetcher = () => this.ipIntelligence!.getIntelligence(ip);
    if (this.options.cacheTtl && this.options.cacheTtl > 0) {
      return this.getCached(`ipIntelligence:${ip}`, fetcher);
    }
    return fetcher();
  }

  /**
   * Internal caching helper
   */
  private async getCached<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
    const now = Date.now();
    const entry = this.cache.get(key);
    if (entry && entry.expiry > now) {
      return entry.value;
    }
    const value = await fetcher();
    const ttl = this.options.cacheTtl || 0;
    this.cache.set(key, { value, expiry: now + ttl });
    return value;
  }
}
