import * as plugins from './smartnetwork.plugins.js';
import { getLogger } from './logging.js';

// MaxMind types re-exported from mmdb-lib via maxmind
import type { CityResponse, AsnResponse, Reader } from 'maxmind';

/**
 * Unified result from all IP intelligence layers
 */
export interface IIpIntelligenceResult {
  // ASN (Team Cymru primary, MaxMind fallback)
  asn: number | null;
  asnOrg: string | null;

  // Registration (RDAP)
  registrantOrg: string | null;
  registrantCountry: string | null;
  networkRange: string | null;
  abuseContact: string | null;

  // Geolocation (MaxMind GeoLite2 City)
  country: string | null;
  countryCode: string | null;
  city: string | null;
  latitude: number | null;
  longitude: number | null;
  accuracyRadius: number | null;
  timezone: string | null;
}

/**
 * Options for IpIntelligence
 */
export interface IIpIntelligenceOptions {
  /** Max age (ms) before triggering background MMDB refresh. Default: 7 days */
  dbMaxAge?: number;
  /** Timeout (ms) for RDAP/DNS/CDN requests. Default: 5000 */
  timeout?: number;
}

// CDN URLs for GeoLite2 MMDB files (served via jsDelivr from npm packages)
const CITY_MMDB_URL = 'https://cdn.jsdelivr.net/npm/@ip-location-db/geolite2-city-mmdb/geolite2-city-ipv4.mmdb';
const ASN_MMDB_URL = 'https://cdn.jsdelivr.net/npm/@ip-location-db/geolite2-asn-mmdb/geolite2-asn-ipv4.mmdb';

// IANA bootstrap for RDAP
const IANA_BOOTSTRAP_IPV4_URL = 'https://data.iana.org/rdap/ipv4.json';

const DEFAULT_DB_MAX_AGE = 7 * 24 * 60 * 60 * 1000; // 7 days
const DEFAULT_TIMEOUT = 5000;

/**
 * Parsed IANA bootstrap entry: a CIDR prefix mapped to an RDAP base URL
 */
interface IBootstrapEntry {
  prefix: string;
  prefixNum: number;  // numeric representation of the network address
  maskBits: number;
  baseUrl: string;
}

/**
 * IpIntelligence provides IP address intelligence by combining three data sources:
 * - RDAP (direct to RIRs) for registration/org data
 * - Team Cymru DNS for ASN
 * - MaxMind GeoLite2 (in-memory MMDB) for geolocation
 */
export class IpIntelligence {
  private readonly logger = getLogger();
  private readonly dbMaxAge: number;
  private readonly timeout: number;

  // MaxMind readers (lazily initialized)
  private cityReader: Reader<CityResponse> | null = null;
  private asnReader: Reader<AsnResponse> | null = null;
  private lastFetchTime = 0;
  private refreshPromise: Promise<void> | null = null;

  // RDAP bootstrap cache
  private bootstrapEntries: IBootstrapEntry[] | null = null;
  private bootstrapPromise: Promise<void> | null = null;

  constructor(options?: IIpIntelligenceOptions) {
    this.dbMaxAge = options?.dbMaxAge ?? DEFAULT_DB_MAX_AGE;
    this.timeout = options?.timeout ?? DEFAULT_TIMEOUT;
  }

  /**
   * Get comprehensive IP intelligence for the given IP address.
   * Runs RDAP, Team Cymru DNS, and MaxMind lookups in parallel.
   */
  public async getIntelligence(ip: string): Promise<IIpIntelligenceResult> {
    const result: IIpIntelligenceResult = {
      asn: null,
      asnOrg: null,
      registrantOrg: null,
      registrantCountry: null,
      networkRange: null,
      abuseContact: null,
      country: null,
      countryCode: null,
      city: null,
      latitude: null,
      longitude: null,
      accuracyRadius: null,
      timezone: null,
    };

    // Run all three layers in parallel
    const [rdapResult, cymruResult, maxmindResult] = await Promise.allSettled([
      this.queryRdap(ip),
      this.queryTeamCymru(ip),
      this.queryMaxMind(ip),
    ]);

    // Merge RDAP results
    if (rdapResult.status === 'fulfilled' && rdapResult.value) {
      const rdap = rdapResult.value;
      result.registrantOrg = rdap.registrantOrg;
      result.registrantCountry = rdap.registrantCountry;
      result.networkRange = rdap.networkRange;
      result.abuseContact = rdap.abuseContact;
    }

    // Merge Team Cymru results (primary for ASN)
    if (cymruResult.status === 'fulfilled' && cymruResult.value) {
      const cymru = cymruResult.value;
      result.asn = cymru.asn;
    }

    // Merge MaxMind results
    if (maxmindResult.status === 'fulfilled' && maxmindResult.value) {
      const mm = maxmindResult.value;
      result.country = mm.country;
      result.countryCode = mm.countryCode;
      result.city = mm.city;
      result.latitude = mm.latitude;
      result.longitude = mm.longitude;
      result.accuracyRadius = mm.accuracyRadius;
      result.timezone = mm.timezone;

      // Use MaxMind ASN as fallback if Team Cymru failed
      if (result.asn === null && mm.asn !== null) {
        result.asn = mm.asn;
      }
      if (mm.asnOrg) {
        result.asnOrg = mm.asnOrg;
      }
    }

    // If we got ASN from Team Cymru but not org, and MaxMind didn't provide org either,
    // the asnOrg remains null (we don't do an additional lookup)

    return result;
  }

  // ─── RDAP Subsystem ─────────────────────────────────────────────────

  /**
   * Load and cache the IANA RDAP bootstrap file
   */
  private async ensureBootstrap(): Promise<void> {
    if (this.bootstrapEntries) return;
    if (this.bootstrapPromise) {
      await this.bootstrapPromise;
      return;
    }

    this.bootstrapPromise = (async () => {
      try {
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), this.timeout);
        try {
          const response = await fetch(IANA_BOOTSTRAP_IPV4_URL, {
            signal: controller.signal,
            headers: { 'User-Agent': '@push.rocks/smartnetwork' },
          });
          if (!response.ok) throw new Error(`HTTP ${response.status}`);
          const data = await response.json() as { services: [string[], string[]][] };

          const entries: IBootstrapEntry[] = [];
          for (const [prefixes, urls] of data.services) {
            const baseUrl = urls[0]; // first URL is preferred
            for (const prefix of prefixes) {
              const [network, bits] = prefix.split('/');
              entries.push({
                prefix,
                prefixNum: this.ipToNumber(network),
                maskBits: parseInt(bits, 10),
                baseUrl: baseUrl.replace(/\/$/, ''), // strip trailing slash
              });
            }
          }

          // Sort by mask bits descending for longest-prefix match
          entries.sort((a, b) => b.maskBits - a.maskBits);
          this.bootstrapEntries = entries;
        } finally {
          clearTimeout(timeoutId);
        }
      } catch (err: any) {
        this.logger.debug?.(`Failed to load RDAP bootstrap: ${err.message}`);
        this.bootstrapEntries = []; // empty = all RDAP lookups will skip
      }
    })();

    await this.bootstrapPromise;
    this.bootstrapPromise = null;
  }

  /**
   * Find the RDAP base URL for a given IP via longest-prefix match
   */
  private matchRir(ip: string): string | null {
    if (!this.bootstrapEntries || this.bootstrapEntries.length === 0) return null;

    const ipNum = this.ipToNumber(ip);

    for (const entry of this.bootstrapEntries) {
      const mask = (0xFFFFFFFF << (32 - entry.maskBits)) >>> 0;
      if ((ipNum & mask) === (entry.prefixNum & mask)) {
        return entry.baseUrl;
      }
    }

    return null;
  }

  /**
   * Query RDAP for registration data
   */
  private async queryRdap(ip: string): Promise<{
    registrantOrg: string | null;
    registrantCountry: string | null;
    networkRange: string | null;
    abuseContact: string | null;
  } | null> {
    await this.ensureBootstrap();
    const baseUrl = this.matchRir(ip);
    if (!baseUrl) return null;

    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), this.timeout);
    try {
      const response = await fetch(`${baseUrl}/ip/${ip}`, {
        signal: controller.signal,
        headers: {
          'Accept': 'application/rdap+json',
          'User-Agent': '@push.rocks/smartnetwork',
        },
      });
      if (!response.ok) return null;
      const data = await response.json() as any;

      let registrantOrg: string | null = null;
      let registrantCountry: string | null = data.country || null;
      let abuseContact: string | null = null;

      // Parse network range
      let networkRange: string | null = null;
      if (data.cidr0_cidrs && data.cidr0_cidrs.length > 0) {
        const cidr = data.cidr0_cidrs[0];
        networkRange = `${cidr.v4prefix || cidr.v6prefix}/${cidr.length}`;
      } else if (data.startAddress && data.endAddress) {
        networkRange = `${data.startAddress} - ${data.endAddress}`;
      }

      // Parse entities
      if (data.entities && Array.isArray(data.entities)) {
        for (const entity of data.entities) {
          const roles: string[] = entity.roles || [];

          if (roles.includes('registrant') || roles.includes('administrative')) {
            const orgName = this.extractVcardFn(entity);
            if (orgName) registrantOrg = orgName;

            // Try to get country from registrant address if not at top level
            if (!registrantCountry) {
              registrantCountry = this.extractVcardCountry(entity);
            }
          }

          if (roles.includes('abuse')) {
            abuseContact = this.extractVcardEmail(entity);
            // Check nested entities for abuse contact
            if (!abuseContact && entity.entities) {
              for (const subEntity of entity.entities) {
                const subRoles: string[] = subEntity.roles || [];
                if (subRoles.includes('abuse')) {
                  abuseContact = this.extractVcardEmail(subEntity);
                  if (abuseContact) break;
                }
              }
            }
          }
        }
      }

      return { registrantOrg, registrantCountry, networkRange, abuseContact };
    } catch (err: any) {
      this.logger.debug?.(`RDAP query failed for ${ip}: ${err.message}`);
      return null;
    } finally {
      clearTimeout(timeoutId);
    }
  }

  /**
   * Extract the 'fn' (formatted name) from an entity's vcardArray
   */
  private extractVcardFn(entity: any): string | null {
    if (!entity.vcardArray || !Array.isArray(entity.vcardArray)) return null;
    const properties = entity.vcardArray[1];
    if (!Array.isArray(properties)) return null;

    for (const prop of properties) {
      if (Array.isArray(prop) && prop[0] === 'fn') {
        return prop[3] || null;
      }
    }
    return null;
  }

  /**
   * Extract email from an entity's vcardArray
   */
  private extractVcardEmail(entity: any): string | null {
    if (!entity.vcardArray || !Array.isArray(entity.vcardArray)) return null;
    const properties = entity.vcardArray[1];
    if (!Array.isArray(properties)) return null;

    for (const prop of properties) {
      if (Array.isArray(prop) && prop[0] === 'email') {
        return prop[3] || null;
      }
    }
    return null;
  }

  /**
   * Extract country from an entity's vcardArray address field
   */
  private extractVcardCountry(entity: any): string | null {
    if (!entity.vcardArray || !Array.isArray(entity.vcardArray)) return null;
    const properties = entity.vcardArray[1];
    if (!Array.isArray(properties)) return null;

    for (const prop of properties) {
      if (Array.isArray(prop) && prop[0] === 'adr') {
        // The label parameter often contains the full address with country at the end
        const label = prop[1]?.label;
        if (typeof label === 'string') {
          const lines = label.split('\n');
          const lastLine = lines[lines.length - 1]?.trim();
          if (lastLine && lastLine.length > 1) return lastLine;
        }
        // Also check the structured value (7-element array, last element is country)
        const value = prop[3];
        if (Array.isArray(value) && value.length >= 7 && value[6]) {
          return value[6];
        }
      }
    }
    return null;
  }

  // ─── Team Cymru DNS Subsystem ───────────────────────────────────────

  /**
   * Query Team Cymru DNS for ASN information.
   * Query format: reversed.ip.origin.asn.cymru.com TXT
   * Response: "ASN | prefix | CC | rir | date"
   */
  private async queryTeamCymru(ip: string): Promise<{ asn: number; prefix: string; country: string } | null> {
    try {
      const reversed = ip.split('.').reverse().join('.');
      const queryName = `${reversed}.origin.asn.cymru.com`;

      const dnsClient = new plugins.smartdns.dnsClientMod.Smartdns({
        strategy: 'prefer-system',
        allowDohFallback: true,
        timeoutMs: this.timeout,
      });

      const records = await dnsClient.getRecordsTxt(queryName);
      if (!records || records.length === 0) return null;

      // Parse the first TXT record: "13335 | 1.1.1.0/24 | AU | apnic | 2011-08-11"
      const txt = records[0].value || (records[0] as any).data;
      if (!txt) return null;

      const parts = txt.split('|').map((s: string) => s.trim());
      if (parts.length < 3) return null;

      const asn = parseInt(parts[0], 10);
      if (isNaN(asn)) return null;

      return {
        asn,
        prefix: parts[1] || '',
        country: parts[2] || '',
      };
    } catch (err: any) {
      this.logger.debug?.(`Team Cymru DNS query failed for ${ip}: ${err.message}`);
      return null;
    }
  }

  // ─── MaxMind GeoLite2 Subsystem ─────────────────────────────────────

  /**
   * Ensure MMDB readers are initialized. Downloads on first call,
   * triggers background refresh if stale.
   */
  private async ensureReaders(): Promise<void> {
    if (this.cityReader && this.asnReader) {
      // Check if refresh needed
      if (Date.now() - this.lastFetchTime > this.dbMaxAge && !this.refreshPromise) {
        this.refreshPromise = this.downloadAndInitReaders()
          .catch((err) => this.logger.debug?.(`Background MMDB refresh failed: ${err.message}`))
          .finally(() => { this.refreshPromise = null; });
      }
      return;
    }

    // First time: blocking download
    if (this.refreshPromise) {
      await this.refreshPromise;
      return;
    }

    this.refreshPromise = this.downloadAndInitReaders();
    await this.refreshPromise;
    this.refreshPromise = null;
  }

  /**
   * Download MMDB files from CDN and create Reader instances
   */
  private async downloadAndInitReaders(): Promise<void> {
    const [cityBuffer, asnBuffer] = await Promise.all([
      this.fetchBuffer(CITY_MMDB_URL),
      this.fetchBuffer(ASN_MMDB_URL),
    ]);

    this.cityReader = new plugins.maxmind.Reader<CityResponse>(cityBuffer);
    this.asnReader = new plugins.maxmind.Reader<AsnResponse>(asnBuffer);
    this.lastFetchTime = Date.now();
    this.logger.info?.('MaxMind MMDB databases loaded into memory');
  }

  /**
   * Fetch a URL and return the response as a Buffer
   */
  private async fetchBuffer(url: string): Promise<Buffer> {
    const response = await fetch(url, {
      headers: { 'User-Agent': '@push.rocks/smartnetwork' },
    });
    if (!response.ok) {
      throw new Error(`Failed to fetch ${url}: HTTP ${response.status}`);
    }
    const arrayBuffer = await response.arrayBuffer();
    return Buffer.from(arrayBuffer);
  }

  /**
   * Query MaxMind for geo + ASN data
   */
  private async queryMaxMind(ip: string): Promise<{
    country: string | null;
    countryCode: string | null;
    city: string | null;
    latitude: number | null;
    longitude: number | null;
    accuracyRadius: number | null;
    timezone: string | null;
    asn: number | null;
    asnOrg: string | null;
  } | null> {
    try {
      await this.ensureReaders();
    } catch (err: any) {
      this.logger.debug?.(`Failed to initialize MaxMind readers: ${err.message}`);
      return null;
    }

    if (!this.cityReader || !this.asnReader) return null;

    let country: string | null = null;
    let countryCode: string | null = null;
    let city: string | null = null;
    let latitude: number | null = null;
    let longitude: number | null = null;
    let accuracyRadius: number | null = null;
    let timezone: string | null = null;
    let asn: number | null = null;
    let asnOrg: string | null = null;

    // City lookup
    try {
      const cityResult = this.cityReader.get(ip);
      if (cityResult) {
        country = cityResult.country?.names?.en || null;
        countryCode = cityResult.country?.iso_code || null;
        city = cityResult.city?.names?.en || null;
        latitude = cityResult.location?.latitude ?? null;
        longitude = cityResult.location?.longitude ?? null;
        accuracyRadius = cityResult.location?.accuracy_radius ?? null;
        timezone = cityResult.location?.time_zone || null;
      }
    } catch (err: any) {
      this.logger.debug?.(`MaxMind city lookup failed for ${ip}: ${err.message}`);
    }

    // ASN lookup
    try {
      const asnResult = this.asnReader.get(ip);
      if (asnResult) {
        asn = asnResult.autonomous_system_number ?? null;
        asnOrg = asnResult.autonomous_system_organization || null;
      }
    } catch (err: any) {
      this.logger.debug?.(`MaxMind ASN lookup failed for ${ip}: ${err.message}`);
    }

    return { country, countryCode, city, latitude, longitude, accuracyRadius, timezone, asn, asnOrg };
  }

  // ─── Utilities ──────────────────────────────────────────────────────

  /**
   * Convert an IPv4 address string to a 32-bit unsigned number
   */
  private ipToNumber(ip: string): number {
    const parts = ip.split('.');
    return (
      ((parseInt(parts[0], 10) << 24) |
       (parseInt(parts[1], 10) << 16) |
       (parseInt(parts[2], 10) << 8) |
        parseInt(parts[3], 10)) >>> 0
    );
  }
}
