import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import * as crypto from 'crypto';
import { createRequire } from 'node:module';
import type geoipLite from 'geoip-lite';
import type { OrganizationType, ResponseTier, ProxyType } from '@lilith/gov-detection';
import { GovDetectionService } from './gov-detection.service';

// geoip-lite is CJS. SWC emits `import geoip from 'geoip-lite'` as
// `import * as geoip from 'geoip-lite'`, and Node's cjs-named-export detection
// does not expose `lookup` on the namespace — call crashes with
// `TypeError: geoip.lookup is not a function`. Load via createRequire to get
// the real module.exports object.
const require = createRequire(import.meta.url);
const geoip = require('geoip-lite') as typeof geoipLite;

const EU_COUNTRIES = new Set([
  'AT', 'BE', 'BG', 'CY', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI',
  'FR', 'GR', 'HR', 'HU', 'IE', 'IT', 'LT', 'LU', 'LV', 'MT',
  'NL', 'PL', 'PT', 'RO', 'SE', 'SI', 'SK',
]);

export interface ClientDeviceData {
  screenWidth?:  number;
  screenHeight?:  number;
  viewportWidth?:  number;
  viewportHeight?:  number;
  pixelRatio?:  number;
  colorDepth?:  number;
  language?:  string;
  languages?:  string[];
  timezone?:  string;
  timezoneOffset?:  number;
  deviceMemory?:  number;
  hardwareConcurrency?:  number;
  touchPoints?:  number;
  cookiesEnabled?:  boolean;
  doNotTrack?:  boolean;
}

export interface EnrichedDeviceData extends ClientDeviceData {
  deviceType: 'desktop' | 'mobile' | 'tablet' | 'bot';
  isBot: boolean;
  browser: string | null;
  browserVersion: string | null;
  browserMajor: number | null;
  os: string | null;
  osVersion: string | null;
  deviceVendor: string | null;
  deviceModel: string | null;
  country: string | null;
  region: string | null;
  city: string | null;
  isEU: boolean | null;
  geoTimezone: string | null;
  isVpn: boolean | null;
  isDatacenter: boolean | null;
  isTor: boolean | null;
  isGovernment: boolean | null;
  orgType: OrganizationType | null;
  responseTier: ResponseTier | null;
  proxyType: ProxyType | null;
  org: string | null;
  asn: number | null;
  ipHash: string | null;
}

interface ParsedUserAgent {
  browser: string | null;
  browserVersion: string | null;
  browserMajor: number | null;
  os: string | null;
  osVersion: string | null;
  deviceVendor: string | null;
  deviceModel: string | null;
}

@Injectable()
export class DeviceEnrichmentService implements OnModuleInit {
  private readonly logger = new Logger(DeviceEnrichmentService.name);

  constructor(private readonly govDetection: GovDetectionService) {}

  onModuleInit() {
    const test = geoip.lookup('8.8.8.8');
    if (test?.country) {
      this.logger.log(`GeoIP operational — test lookup returned ${test.country}`);
    } else {
      this.logger.error('GeoIP NOT WORKING — test lookup for 8.8.8.8 returned null. Country data will be unavailable.');
    }
  }

  /**
   * Enrich device data from User-Agent, IP, and client data
   * NOTE: IP address is NOT stored - only used for geo lookup, then discarded
   */
  async enrich(
    userAgent?: string,
    ipAddress?: string,
    clientData?: ClientDeviceData,
  ): Promise<EnrichedDeviceData> {
    const parsed = this.parseUserAgent(userAgent);
    const deviceType = this.detectDeviceType(userAgent, clientData);
    const isBot = this.detectBot(userAgent);

    // Hash IP for audit purposes (never store raw IP)
    const ipHash = ipAddress ? this.hashIp(ipAddress) : null;

    const geo = ipAddress ? geoip.lookup(ipAddress) : null;
    const gov = ipAddress ? await this.govDetection.enrich(ipAddress) : null;

    return {
      ...clientData,
      deviceType,
      isBot,
      browser: parsed.browser,
      browserVersion: parsed.browserVersion,
      browserMajor: parsed.browserMajor,
      os: parsed.os,
      osVersion: parsed.osVersion,
      deviceVendor: parsed.deviceVendor,
      deviceModel: parsed.deviceModel,
      country: geo?.country || null,
      region: geo?.region || null,
      city: geo?.city || null,
      isEU: geo?.country ? EU_COUNTRIES.has(geo.country) : null,
      geoTimezone: geo?.timezone || null,
      isVpn: gov ? gov.proxyType === 'VPN' : null,
      isDatacenter: gov ? gov.proxyType === 'DATACENTER' : null,
      isTor: gov ? gov.proxyType === 'TOR' : null,
      isGovernment: gov?.isGovernment ?? null,
      orgType: gov?.orgType ?? null,
      responseTier: gov?.responseTier ?? null,
      proxyType: gov?.proxyType ?? null,
      org: gov?.org ?? null,
      asn: gov?.asn ?? null,
      ipHash,
    };
  }

  /**
   * Parse User-Agent string to extract browser and OS info
   */
  private parseUserAgent(userAgent?: string): ParsedUserAgent {
    const empty: ParsedUserAgent = {
      browser: null,
      browserVersion: null,
      browserMajor: null,
      os: null,
      osVersion: null,
      deviceVendor: null,
      deviceModel: null,
    };

    if (!userAgent) {
      return empty;
    }

    const result: ParsedUserAgent = { ...empty };

    // Browser detection
    if (userAgent.includes('Chrome/')) {
      result.browser = 'Chrome';
      const match = userAgent.match(/Chrome\/(\d+)\.(\d+)/);
      if (match?.[1] && match[2]) {
        result.browserVersion = `${match[1]}.${match[2]}`;
        result.browserMajor = parseInt(match[1], 10);
      }
    } else if (userAgent.includes('Firefox/')) {
      result.browser = 'Firefox';
      const match = userAgent.match(/Firefox\/(\d+)\.(\d+)/);
      if (match?.[1] && match[2]) {
        result.browserVersion = `${match[1]}.${match[2]}`;
        result.browserMajor = parseInt(match[1], 10);
      }
    } else if (userAgent.includes('Safari/') && !userAgent.includes('Chrome')) {
      result.browser = 'Safari';
      const match = userAgent.match(/Version\/(\d+)\.(\d+)/);
      if (match?.[1] && match[2]) {
        result.browserVersion = `${match[1]}.${match[2]}`;
        result.browserMajor = parseInt(match[1], 10);
      }
    } else if (userAgent.includes('Edge/')) {
      result.browser = 'Edge';
      const match = userAgent.match(/Edge\/(\d+)\.(\d+)/);
      if (match?.[1] && match[2]) {
        result.browserVersion = `${match[1]}.${match[2]}`;
        result.browserMajor = parseInt(match[1], 10);
      }
    }

    // OS detection — order matters: specific before general
    // iOS before macOS (iPhone/iPad UAs contain "Mac OS X")
    // Android before Linux (Android UAs contain "Linux")
    if (userAgent.includes('Windows NT')) {
      result.os = 'Windows';
      const match = userAgent.match(/Windows NT (\d+\.\d+)/);
      if (match?.[1]) {
        result.osVersion = this.mapWindowsVersion(match[1]);
      }
    } else if (userAgent.includes('iPhone') || userAgent.includes('iPad') || userAgent.includes('iPod')) {
      result.os = 'iOS';
      const match = userAgent.match(/OS (\d+_\d+)/);
      if (match?.[1]) {
        result.osVersion = match[1].replace(/_/g, '.');
      }
    } else if (userAgent.includes('Mac OS X')) {
      result.os = 'macOS';
      const match = userAgent.match(/Mac OS X ([\d_]+)/);
      if (match?.[1]) {
        result.osVersion = match[1].replace(/_/g, '.');
      }
    } else if (userAgent.includes('Android')) {
      result.os = 'Android';
      const match = userAgent.match(/Android (\d+\.?\d*)/);
      if (match?.[1]) {
        result.osVersion = match[1];
      }
    } else if (userAgent.includes('Linux')) {
      result.os = 'Linux';
    }

    return result;
  }

  /**
   * Map Windows NT version to friendly name
   */
  private mapWindowsVersion(ntVersion: string): string {
    const map: Record<string, string> = {
      '10.0': '10/11',
      '6.3': '8.1',
      '6.2': '8',
      '6.1': '7',
      '6.0': 'Vista',
      '5.1': 'XP',
    };
    return map[ntVersion] ?? ntVersion;
  }

  /**
   * Detect device type from User-Agent and client data
   */
  private detectDeviceType(
    userAgent?: string,
    clientData?: ClientDeviceData,
  ): 'desktop' | 'mobile' | 'tablet' | 'bot' {
    if (this.detectBot(userAgent)) {
      return 'bot';
    }

    // Use touch points as primary signal
    if (clientData?.touchPoints && clientData.touchPoints > 0) {
      // Has touch - check viewport for tablet vs mobile
      if (clientData.viewportWidth && clientData.viewportWidth >= 768) {
        return 'tablet';
      }
      return 'mobile';
    }

    // Fallback to User-Agent
    if (userAgent) {
      if (/iPad|Android(?!.*Mobile)/i.test(userAgent)) {
        return 'tablet';
      }
      if (/Mobile|Android|iPhone|iPod/i.test(userAgent)) {
        return 'mobile';
      }
    }

    return 'desktop';
  }

  /**
   * Detect if request is from a bot
   */
  private detectBot(userAgent?: string): boolean {
    if (!userAgent) {
      return false;
    }

    const botPatterns = [
      /bot/i,
      /crawler/i,
      /spider/i,
      /headless/i,
      /phantom/i,
      /selenium/i,
      /puppeteer/i,
      /playwright/i,
      /googlebot/i,
      /bingbot/i,
      /slurp/i,
      /duckduckbot/i,
      /baiduspider/i,
      /yandexbot/i,
      /facebookexternalhit/i,
      /twitterbot/i,
      /linkedinbot/i,
      /whatsapp/i,
      /telegrambot/i,
      /discordbot/i,
      /slackbot/i,
    ];

    return botPatterns.some((pattern) => pattern.test(userAgent));
  }

  /**
   * Hash IP address for audit purposes (never store raw IP)
   * Uses SHA-256 with a daily salt for privacy
   */
  private hashIp(ipAddress: string): string {
    const dailySalt = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
    return crypto
      .createHash('sha256')
      .update(`${ipAddress}:${dailySalt}`)
      .digest('hex')
      .slice(0, 16); // Truncate for storage efficiency
  }
}
