import * as plugins from '../plugins.js';
import { logger } from '../logger.js';
import { IpIntelligenceDoc, SecurityBlockRuleDoc, SecurityPolicyAuditDoc } from '../db/index.js';
import type {
  IIpIntelligenceRecord,
  ISecurityBlockRule,
  ISecurityCompiledPolicy,
  ISecurityPolicyAuditEvent,
  TSecurityBlockRuleMatchMode,
  TSecurityBlockRuleType,
} from '../../ts_interfaces/data/security-policy.js';

export interface ISecurityPolicyManagerOptions {
  intelligenceRefreshMs?: number;
  onPolicyChanged?: () => void | Promise<void>;
}

export interface IRemoteIngressFirewallSnapshot {
  blockedIps: string[];
}

const OBSERVED_IP_QUEUE_LIMIT = 512;
const OBSERVED_IP_BATCH_LIMIT = 20;
const OBSERVED_IP_QUEUE_CONCURRENCY = 2;
const OBSERVED_IP_REQUEUE_THROTTLE_MS = 60_000;

export class SecurityPolicyManager {
  private readonly smartNetwork = new plugins.smartnetwork.SmartNetwork({
    cacheTtl: 24 * 60 * 60 * 1000,
    ipIntelligenceTimeout: 5_000,
  });
  private readonly intelligenceRefreshMs: number;
  private readonly inFlightObservations = new Map<string, Promise<void>>();
  private readonly queuedObservations = new Set<string>();
  private readonly observationQueue: string[] = [];
  private readonly lastQueuedAt = new Map<string, number>();
  private activeQueuedObservations = 0;
  private queueDrainScheduled = false;
  private isStopping = false;
  private readonly onPolicyChanged?: () => void | Promise<void>;

  constructor(options: ISecurityPolicyManagerOptions = {}) {
    this.intelligenceRefreshMs = options.intelligenceRefreshMs ?? 24 * 60 * 60 * 1000;
    this.onPolicyChanged = options.onPolicyChanged;
  }

  public async start(): Promise<void> {
    logger.log('info', 'SecurityPolicyManager started');
  }

  public async stop(): Promise<void> {
    this.isStopping = true;
    this.observationQueue.length = 0;
    this.queuedObservations.clear();
    await this.smartNetwork.stop();
  }

  public async observeIps(ips: string[]): Promise<void> {
    const uniqueIps = [...new Set(ips.map((ip) => this.normalizeIp(ip)).filter(Boolean) as string[])];
    await Promise.allSettled(uniqueIps.map((ip) => this.observeIp(ip)));
  }

  public queueObservedIps(ips: string[]): void {
    if (this.isStopping) return;

    const now = Date.now();
    const uniqueIps = [...new Set(ips.map((ip) => this.normalizeIp(ip)).filter(Boolean) as string[])];

    for (const ip of uniqueIps.slice(0, OBSERVED_IP_BATCH_LIMIT)) {
      if (!this.isPublicIp(ip)) continue;
      if (this.inFlightObservations.has(ip) || this.queuedObservations.has(ip)) continue;

      const lastQueuedAt = this.lastQueuedAt.get(ip);
      if (lastQueuedAt && now - lastQueuedAt < OBSERVED_IP_REQUEUE_THROTTLE_MS) continue;

      if (this.observationQueue.length >= OBSERVED_IP_QUEUE_LIMIT) {
        const droppedIp = this.observationQueue.shift();
        if (droppedIp) this.queuedObservations.delete(droppedIp);
      }

      this.observationQueue.push(ip);
      this.queuedObservations.add(ip);
      this.lastQueuedAt.set(ip, now);
    }

    this.pruneQueuedIpMemory(now);
    this.scheduleQueueDrain();
  }

  public async observeIp(ipAddress: string, options: { force?: boolean } = {}): Promise<void> {
    const ip = this.normalizeIp(ipAddress);
    if (!ip || !this.isPublicIp(ip)) {
      return;
    }

    const existingObservation = this.inFlightObservations.get(ip);
    if (existingObservation) {
      await existingObservation;
      if (!options.force) return;
    }

    const observationPromise = this.performObserveIp(ip, options).finally(() => {
      if (this.inFlightObservations.get(ip) === observationPromise) {
        this.inFlightObservations.delete(ip);
      }
    });
    this.inFlightObservations.set(ip, observationPromise);
    await observationPromise;
  }

  private async performObserveIp(ip: string, options: { force?: boolean } = {}): Promise<void> {
    try {
      const now = Date.now();
      let doc = await IpIntelligenceDoc.findByIp(ip);
      if (doc && !options.force && now - doc.updatedAt < this.intelligenceRefreshMs) {
        if (now - doc.lastSeenAt > 60_000) {
          doc.lastSeenAt = now;
          doc.seenCount = (doc.seenCount || 0) + 1;
          await doc.save();
        }
        return;
      }

      const intelligence = await this.smartNetwork.getIpIntelligence(ip);
      if (!doc) {
        doc = new IpIntelligenceDoc();
        doc.ipAddress = ip;
        doc.firstSeenAt = now;
      }
      Object.assign(doc, intelligence);
      doc.lastSeenAt = now;
      doc.updatedAt = now;
      doc.seenCount = (doc.seenCount || 0) + 1;
      await doc.save();

      if (await this.matchesAnyReactiveRule(doc)) {
        await this.notifyPolicyChanged();
      }
    } catch (err) {
      logger.log('warn', `Failed to enrich IP ${ip}: ${(err as Error).message}`);
    }
  }

  public async listBlockRules(): Promise<ISecurityBlockRule[]> {
    return (await SecurityBlockRuleDoc.findAll()).map((doc) => this.ruleFromDoc(doc));
  }

  public async listIpIntelligence(options: { ipAddresses?: string[]; limit?: number } = {}): Promise<IIpIntelligenceRecord[]> {
    const limit = Number.isInteger(options.limit) && options.limit! > 0
      ? Math.min(options.limit!, 500)
      : undefined;

    let docs: IpIntelligenceDoc[];
    if (options.ipAddresses?.length) {
      const ips = [...new Set(options.ipAddresses.map((ip) => this.normalizeIp(ip)).filter(Boolean) as string[])];
      const results = await Promise.all(ips.map((ip) => IpIntelligenceDoc.findByIp(ip)));
      docs = results.filter(Boolean) as IpIntelligenceDoc[];
    } else {
      docs = await IpIntelligenceDoc.findAll();
    }

    const sortedDocs = docs.sort((a, b) => (b.lastSeenAt || 0) - (a.lastSeenAt || 0));
    return (limit ? sortedDocs.slice(0, limit) : sortedDocs).map((doc) => this.intelligenceFromDoc(doc));
  }

  public async refreshIpIntelligence(ipAddress: string): Promise<IIpIntelligenceRecord | null> {
    const ip = this.normalizeIp(ipAddress);
    if (!ip || !this.isPublicIp(ip)) {
      return null;
    }
    await this.observeIp(ip, { force: true });
    const doc = await IpIntelligenceDoc.findByIp(ip);
    return doc ? this.intelligenceFromDoc(doc) : null;
  }

  private scheduleQueueDrain(): void {
    if (this.queueDrainScheduled || this.isStopping) return;
    this.queueDrainScheduled = true;
    setTimeout(() => {
      this.queueDrainScheduled = false;
      this.drainObservationQueue();
    }, 0);
  }

  private drainObservationQueue(): void {
    if (this.isStopping) return;

    while (
      this.activeQueuedObservations < OBSERVED_IP_QUEUE_CONCURRENCY &&
      this.observationQueue.length > 0
    ) {
      const ip = this.observationQueue.shift()!;
      this.queuedObservations.delete(ip);
      this.activeQueuedObservations++;
      void this.observeIp(ip)
        .catch(() => undefined)
        .finally(() => {
          this.activeQueuedObservations--;
          if (this.observationQueue.length > 0) {
            this.scheduleQueueDrain();
          }
        });
    }
  }

  private pruneQueuedIpMemory(now: number): void {
    if (this.lastQueuedAt.size <= OBSERVED_IP_QUEUE_LIMIT * 2) return;
    for (const [ip, lastQueuedAt] of this.lastQueuedAt) {
      if (now - lastQueuedAt > OBSERVED_IP_REQUEUE_THROTTLE_MS * 2) {
        this.lastQueuedAt.delete(ip);
      }
    }
  }

  public async listAuditEvents(limit = 100): Promise<ISecurityPolicyAuditEvent[]> {
    return (await SecurityPolicyAuditDoc.findRecent(limit)).map((doc) => ({
      id: doc.id,
      action: doc.action,
      actor: doc.actor,
      details: doc.details,
      createdAt: doc.createdAt,
    }));
  }

  private intelligenceFromDoc(doc: IpIntelligenceDoc): IIpIntelligenceRecord {
    return {
      ipAddress: doc.ipAddress,
      asn: doc.asn,
      asnOrg: doc.asnOrg,
      registrantOrg: doc.registrantOrg,
      registrantCountry: doc.registrantCountry,
      networkRange: doc.networkRange,
      networkCidrs: doc.networkCidrs,
      abuseContact: doc.abuseContact,
      country: doc.country,
      countryCode: doc.countryCode,
      city: doc.city,
      latitude: doc.latitude,
      longitude: doc.longitude,
      accuracyRadius: doc.accuracyRadius,
      timezone: doc.timezone,
      firstSeenAt: doc.firstSeenAt,
      lastSeenAt: doc.lastSeenAt,
      updatedAt: doc.updatedAt,
      seenCount: doc.seenCount,
    };
  }

  public async createBlockRule(input: {
    type: TSecurityBlockRuleType;
    value: string;
    matchMode?: TSecurityBlockRuleMatchMode;
    reason?: string;
    enabled?: boolean;
  }, actor = 'system'): Promise<ISecurityBlockRule> {
    const now = Date.now();
    const doc = new SecurityBlockRuleDoc();
    doc.id = plugins.uuid.v4();
    doc.type = input.type;
    doc.value = input.value.trim();
    doc.matchMode = input.matchMode;
    doc.reason = input.reason;
    doc.enabled = input.enabled ?? true;
    doc.createdAt = now;
    doc.updatedAt = now;
    doc.createdBy = actor;
    await doc.save();
    await this.writeAudit('createBlockRule', actor, { rule: this.ruleFromDoc(doc) });
    await this.notifyPolicyChanged();
    return this.ruleFromDoc(doc);
  }

  public async updateBlockRule(id: string, patch: Partial<Pick<ISecurityBlockRule, 'value' | 'matchMode' | 'reason' | 'enabled'>>, actor = 'system'): Promise<ISecurityBlockRule | null> {
    const doc = await SecurityBlockRuleDoc.findById(id);
    if (!doc) {
      return null;
    }
    if (patch.value !== undefined) doc.value = patch.value.trim();
    if (patch.matchMode !== undefined) doc.matchMode = patch.matchMode;
    if (patch.reason !== undefined) doc.reason = patch.reason;
    if (patch.enabled !== undefined) doc.enabled = patch.enabled;
    doc.updatedAt = Date.now();
    await doc.save();
    await this.writeAudit('updateBlockRule', actor, { id, patch });
    await this.notifyPolicyChanged();
    return this.ruleFromDoc(doc);
  }

  public async deleteBlockRule(id: string, actor = 'system'): Promise<boolean> {
    const doc = await SecurityBlockRuleDoc.findById(id);
    if (!doc) {
      return false;
    }
    await doc.delete();
    await this.writeAudit('deleteBlockRule', actor, { id });
    await this.notifyPolicyChanged();
    return true;
  }

  public async compilePolicy(): Promise<ISecurityCompiledPolicy> {
    const rules = await SecurityBlockRuleDoc.findEnabled();
    const intelligenceDocs = await IpIntelligenceDoc.findAll();
    const blockedIps = new Set<string>();
    const blockedCidrs = new Set<string>();

    for (const rule of rules) {
      const normalizedValue = rule.value.trim();
      if (!normalizedValue) continue;

      if (rule.type === 'ip') {
        const ip = this.normalizeIp(normalizedValue);
        if (ip && plugins.net.isIP(ip)) blockedIps.add(ip);
        continue;
      }

      if (rule.type === 'cidr') {
        for (const cidr of this.normalizeNetworkEntries(normalizedValue)) {
          blockedCidrs.add(cidr);
        }
        continue;
      }

      for (const doc of intelligenceDocs) {
        if (!this.ruleMatchesIntelligence(rule, doc)) continue;
        const networkEntries = this.normalizeNetworkEntryList([
          ...(doc.networkCidrs || []),
          doc.networkRange,
        ]);
        if (networkEntries.length > 0) {
          for (const cidr of networkEntries) {
            blockedCidrs.add(cidr);
          }
        } else if (this.normalizeIp(doc.ipAddress)) {
          blockedIps.add(this.normalizeIp(doc.ipAddress)!);
        }
      }
    }

    return {
      blockedIps: [...blockedIps].sort(),
      blockedCidrs: [...blockedCidrs].sort(),
    };
  }

  public async compileSmartProxyPolicy(): Promise<ISecurityCompiledPolicy> {
    return await this.compilePolicy();
  }

  public async compileRemoteIngressFirewall(): Promise<IRemoteIngressFirewallSnapshot> {
    const policy = await this.compilePolicy();
    const blockedIps = [
      ...policy.blockedIps.filter((ip) => plugins.net.isIP(ip) === 4),
      ...policy.blockedCidrs.filter((cidr) => plugins.net.isIP(cidr.split('/')[0]) === 4),
    ];
    return { blockedIps };
  }

  private async matchesAnyReactiveRule(doc: IpIntelligenceDoc): Promise<boolean> {
    const rules = await SecurityBlockRuleDoc.findEnabled();
    return rules.some((rule) => rule.type === 'asn' || rule.type === 'organization'
      ? this.ruleMatchesIntelligence(rule, doc)
      : false);
  }

  private ruleMatchesIntelligence(rule: SecurityBlockRuleDoc, doc: IpIntelligenceDoc): boolean {
    const value = rule.value.trim().toLowerCase();
    if (!value) return false;

    if (rule.type === 'asn') {
      return String(doc.asn ?? '') === value.replace(/^as/i, '');
    }

    if (rule.type === 'organization') {
      const candidates = [doc.asnOrg, doc.registrantOrg]
        .filter(Boolean)
        .map((candidate) => candidate!.toLowerCase());
      if (rule.matchMode === 'exact') {
        return candidates.some((candidate) => candidate === value);
      }
      return candidates.some((candidate) => candidate.includes(value));
    }

    return false;
  }

  private normalizeIp(ipAddress: string): string | undefined {
    const ip = ipAddress.trim();
    if (ip.startsWith('::ffff:')) {
      return ip.slice('::ffff:'.length);
    }
    return plugins.net.isIP(ip) ? ip : undefined;
  }

  private normalizeCidr(value: string): string | undefined {
    const [rawIp, rawPrefix] = value.trim().split('/');
    if (!rawIp || !rawPrefix) return undefined;
    const ip = this.normalizeIp(rawIp);
    if (!ip) return undefined;
    const prefix = Number(rawPrefix);
    const maxPrefix = plugins.net.isIP(ip) === 4 ? 32 : 128;
    if (!Number.isInteger(prefix) || prefix < 0 || prefix > maxPrefix) return undefined;
    return `${ip}/${prefix}`;
  }

  private normalizeNetworkEntries(value: string): string[] {
    const trimmed = value.trim();
    if (!trimmed) return [];

    const cidr = this.normalizeCidr(trimmed);
    if (cidr) return [cidr];

    const rangeParts = trimmed.split(/\s+-\s+/);
    if (rangeParts.length === 2) {
      return this.ipv4RangeToCidrs(rangeParts[0], rangeParts[1]);
    }

    return [];
  }

  private normalizeNetworkEntryList(values: Array<string | null | undefined>): string[] {
    const cidrs = new Set<string>();
    for (const value of values) {
      if (!value) continue;
      for (const entry of value.split(',').map((part) => part.trim()).filter(Boolean)) {
        for (const cidr of this.normalizeNetworkEntries(entry)) {
          cidrs.add(cidr);
        }
      }
    }
    return [...cidrs];
  }

  private ipv4RangeToCidrs(startIp: string, endIp: string): string[] {
    const start = this.ipv4ToBigInt(startIp);
    const end = this.ipv4ToBigInt(endIp);
    if (start === undefined || end === undefined || start > end) return [];

    const cidrs: string[] = [];
    let current = start;
    while (current <= end) {
      let maxBlockSize = current === 0n ? 1n << 32n : current & -current;
      const remaining = end - current + 1n;
      while (maxBlockSize > remaining) {
        maxBlockSize = maxBlockSize / 2n;
      }
      const prefixLength = 32 - this.powerOfTwoExponent(maxBlockSize);
      cidrs.push(`${this.numberToIpv4(current)}/${prefixLength}`);
      current += maxBlockSize;
    }
    return cidrs;
  }

  private ipv4ToBigInt(ip: string): bigint | undefined {
    const normalized = this.normalizeIp(ip);
    if (!normalized || plugins.net.isIP(normalized) !== 4) return undefined;
    return normalized
      .split('.')
      .reduce((sum, part) => (sum * 256n) + BigInt(Number(part)), 0n);
  }

  private numberToIpv4(value: bigint): string {
    return [
      Number((value >> 24n) & 255n),
      Number((value >> 16n) & 255n),
      Number((value >> 8n) & 255n),
      Number(value & 255n),
    ].join('.');
  }

  private powerOfTwoExponent(value: bigint): number {
    let exponent = 0;
    let remaining = value;
    while (remaining > 1n) {
      remaining >>= 1n;
      exponent++;
    }
    return exponent;
  }

  private isPublicIp(ip: string): boolean {
    const family = plugins.net.isIP(ip);
    if (family === 4) {
      const parts = ip.split('.').map((part) => Number(part));
      const [a, b] = parts;
      if (a === 10 || a === 127 || a === 0 || a >= 224) return false;
      if (a === 100 && b >= 64 && b <= 127) return false;
      if (a === 169 && b === 254) return false;
      if (a === 172 && b >= 16 && b <= 31) return false;
      if (a === 192 && b === 168) return false;
      return true;
    }
    if (family === 6) {
      const lower = ip.toLowerCase();
      if (lower === '::1' || lower === '::') return false;
      if (lower.startsWith('fe80:') || lower.startsWith('fc') || lower.startsWith('fd')) return false;
      return true;
    }
    return false;
  }

  private ruleFromDoc(doc: SecurityBlockRuleDoc): ISecurityBlockRule {
    return {
      id: doc.id,
      type: doc.type,
      value: doc.value,
      matchMode: doc.matchMode,
      enabled: doc.enabled,
      reason: doc.reason,
      createdAt: doc.createdAt,
      updatedAt: doc.updatedAt,
      createdBy: doc.createdBy,
    };
  }

  private async writeAudit(action: string, actor: string, details: Record<string, unknown>): Promise<void> {
    const doc = new SecurityPolicyAuditDoc();
    doc.id = plugins.uuid.v4();
    doc.action = action;
    doc.actor = actor;
    doc.details = details;
    doc.createdAt = Date.now();
    await doc.save();
  }

  private async notifyPolicyChanged(): Promise<void> {
    if (this.onPolicyChanged) {
      await this.onPolicyChanged();
    }
  }
}
