import * as plugins from '../plugins.js';
import { logger } from '../logger.js';
import { VlanMappingsDoc } from '../db/index.js';

/**
 * MAC address to VLAN mapping
 */
export interface IMacVlanMapping {
  /** MAC address (full) or OUI pattern (e.g., "00:11:22" for vendor prefix) */
  mac: string;
  /** VLAN ID to assign */
  vlan: number;
  /** Optional description */
  description?: string;
  /** Whether this mapping is enabled */
  enabled: boolean;
  /** Creation timestamp */
  createdAt: number;
  /** Last update timestamp */
  updatedAt: number;
}

/**
 * VLAN assignment result
 */
export interface IVlanAssignmentResult {
  /** Whether a VLAN was successfully assigned */
  assigned: boolean;
  /** The assigned VLAN ID (or default if not matched) */
  vlan: number;
  /** The matching rule (if any) */
  matchedRule?: IMacVlanMapping;
  /** Whether default VLAN was used */
  isDefault: boolean;
}

/**
 * VlanManager configuration
 */
export interface IVlanManagerConfig {
  /** Default VLAN for unknown MACs */
  defaultVlan?: number;
  /** Whether to allow unknown MACs (assign default VLAN) or reject */
  allowUnknownMacs?: boolean;
}

/**
 * Manages MAC address to VLAN mappings with support for:
 * - Exact MAC address matching
 * - OUI (vendor prefix) pattern matching
 * - Wildcard patterns
 * - Default VLAN for unknown devices
 */
export class VlanManager {
  private mappings: Map<string, IMacVlanMapping> = new Map();
  private config: Required<IVlanManagerConfig>;

  // Cache for normalized MAC lookups
  private normalizedMacCache: Map<string, string> = new Map();

  constructor(config?: IVlanManagerConfig) {
    this.config = {
      defaultVlan: config?.defaultVlan ?? 1,
      allowUnknownMacs: config?.allowUnknownMacs ?? true,
    };
  }

  /**
   * Initialize the VLAN manager and load persisted mappings
   */
  async initialize(): Promise<void> {
    await this.loadMappings();
    logger.log('info', `VlanManager initialized with ${this.mappings.size} mappings, default VLAN: ${this.config.defaultVlan}`);
  }

  /**
   * Normalize a MAC address to lowercase with colons
   * Accepts formats: 00:11:22:33:44:55, 00-11-22-33-44-55, 001122334455
   */
  normalizeMac(mac: string): string {
    // Check cache first
    const cached = this.normalizedMacCache.get(mac);
    if (cached) {
      return cached;
    }

    // Remove all separators and convert to lowercase
    const cleaned = mac.toLowerCase().replace(/[-:]/g, '');

    // Format with colons
    const normalized = cleaned.match(/.{1,2}/g)?.join(':') || mac.toLowerCase();

    // Cache the result
    this.normalizedMacCache.set(mac, normalized);

    // Prevent unbounded cache growth
    if (this.normalizedMacCache.size > 10000) {
      const iterator = this.normalizedMacCache.keys();
      for (let i = 0; i < 1000; i++) {
        this.normalizedMacCache.delete(iterator.next().value!);
      }
    }

    return normalized;
  }

  /**
   * Check if a MAC address matches a pattern
   * Supports:
   * - Exact match: "00:11:22:33:44:55"
   * - OUI match: "00:11:22" (matches any device with this vendor prefix)
   * - Wildcard: "*" (matches all)
   */
  macMatchesPattern(mac: string, pattern: string): boolean {
    const normalizedMac = this.normalizeMac(mac);
    const normalizedPattern = this.normalizeMac(pattern);

    // Wildcard matches all
    if (pattern === '*') {
      return true;
    }

    // Exact match
    if (normalizedMac === normalizedPattern) {
      return true;
    }

    // OUI/prefix match (pattern is shorter than full MAC)
    if (normalizedPattern.length < 17 && normalizedMac.startsWith(normalizedPattern)) {
      return true;
    }

    return false;
  }

  /**
   * Add or update a MAC to VLAN mapping
   */
  async addMapping(mapping: Omit<IMacVlanMapping, 'createdAt' | 'updatedAt'>): Promise<IMacVlanMapping> {
    const normalizedMac = this.normalizeMac(mapping.mac);
    const now = Date.now();

    const existingMapping = this.mappings.get(normalizedMac);
    const fullMapping: IMacVlanMapping = {
      ...mapping,
      mac: normalizedMac,
      createdAt: existingMapping?.createdAt || now,
      updatedAt: now,
    };

    this.mappings.set(normalizedMac, fullMapping);

    // Persist to database
    await this.saveMappings();

    logger.log('info', `VLAN mapping ${existingMapping ? 'updated' : 'added'}: ${normalizedMac} -> VLAN ${mapping.vlan}`);
    return fullMapping;
  }

  /**
   * Remove a MAC to VLAN mapping
   */
  async removeMapping(mac: string): Promise<boolean> {
    const normalizedMac = this.normalizeMac(mac);
    const removed = this.mappings.delete(normalizedMac);

    if (removed) {
      await this.saveMappings();
      logger.log('info', `VLAN mapping removed: ${normalizedMac}`);
    }

    return removed;
  }

  /**
   * Get a specific mapping by MAC
   */
  getMapping(mac: string): IMacVlanMapping | undefined {
    return this.mappings.get(this.normalizeMac(mac));
  }

  /**
   * Get all mappings
   */
  getAllMappings(): IMacVlanMapping[] {
    return Array.from(this.mappings.values());
  }

  /**
   * Determine VLAN assignment for a MAC address
   * Returns the most specific matching rule (exact > OUI > wildcard > default)
   */
  assignVlan(mac: string): IVlanAssignmentResult {
    const normalizedMac = this.normalizeMac(mac);

    // First, try exact match
    const exactMatch = this.mappings.get(normalizedMac);
    if (exactMatch && exactMatch.enabled) {
      return {
        assigned: true,
        vlan: exactMatch.vlan,
        matchedRule: exactMatch,
        isDefault: false,
      };
    }

    // Try OUI/prefix matches (sorted by specificity - longer patterns first)
    const patternMatches: IMacVlanMapping[] = [];
    for (const mapping of this.mappings.values()) {
      if (mapping.enabled && mapping.mac !== normalizedMac && this.macMatchesPattern(normalizedMac, mapping.mac)) {
        patternMatches.push(mapping);
      }
    }

    // Sort by pattern length (most specific first)
    patternMatches.sort((a, b) => b.mac.length - a.mac.length);

    if (patternMatches.length > 0) {
      const bestMatch = patternMatches[0];
      return {
        assigned: true,
        vlan: bestMatch.vlan,
        matchedRule: bestMatch,
        isDefault: false,
      };
    }

    // No match - use default VLAN if allowed
    if (this.config.allowUnknownMacs) {
      return {
        assigned: true,
        vlan: this.config.defaultVlan,
        isDefault: true,
      };
    }

    // Unknown MAC and not allowed
    return {
      assigned: false,
      vlan: 0,
      isDefault: false,
    };
  }

  /**
   * Bulk import mappings
   */
  async importMappings(mappings: Array<Omit<IMacVlanMapping, 'createdAt' | 'updatedAt'>>): Promise<number> {
    let imported = 0;

    for (const mapping of mappings) {
      await this.addMapping(mapping);
      imported++;
    }

    logger.log('info', `Imported ${imported} VLAN mappings`);
    return imported;
  }

  /**
   * Export all mappings
   */
  exportMappings(): IMacVlanMapping[] {
    return this.getAllMappings();
  }

  /**
   * Update configuration
   */
  updateConfig(config: Partial<IVlanManagerConfig>): void {
    if (config.defaultVlan !== undefined) {
      this.config.defaultVlan = config.defaultVlan;
    }
    if (config.allowUnknownMacs !== undefined) {
      this.config.allowUnknownMacs = config.allowUnknownMacs;
    }
    logger.log('info', `VlanManager config updated: defaultVlan=${this.config.defaultVlan}, allowUnknown=${this.config.allowUnknownMacs}`);
  }

  /**
   * Get current configuration
   */
  getConfig(): Required<IVlanManagerConfig> {
    return { ...this.config };
  }

  /**
   * Get statistics
   */
  getStats(): {
    totalMappings: number;
    enabledMappings: number;
    exactMatches: number;
    ouiPatterns: number;
    wildcardPatterns: number;
  } {
    let exactMatches = 0;
    let ouiPatterns = 0;
    let wildcardPatterns = 0;
    let enabledMappings = 0;

    for (const mapping of this.mappings.values()) {
      if (mapping.enabled) {
        enabledMappings++;
      }

      if (mapping.mac === '*') {
        wildcardPatterns++;
      } else if (mapping.mac.length < 17) {
        // OUI patterns are shorter than full MAC (17 chars with colons)
        ouiPatterns++;
      } else {
        exactMatches++;
      }
    }

    return {
      totalMappings: this.mappings.size,
      enabledMappings,
      exactMatches,
      ouiPatterns,
      wildcardPatterns,
    };
  }

  /**
   * Load mappings from database
   */
  private async loadMappings(): Promise<void> {
    try {
      const doc = await VlanMappingsDoc.load();
      if (doc && Array.isArray(doc.mappings)) {
        for (const mapping of doc.mappings) {
          this.mappings.set(this.normalizeMac(mapping.mac), mapping);
        }
        logger.log('info', `Loaded ${doc.mappings.length} VLAN mappings from database`);
      }
    } catch (error: unknown) {
      logger.log('warn', `Failed to load VLAN mappings from database: ${(error as Error).message}`);
    }
  }

  /**
   * Save mappings to database
   */
  private async saveMappings(): Promise<void> {
    try {
      const mappings = Array.from(this.mappings.values());
      let doc = await VlanMappingsDoc.load();
      if (!doc) {
        doc = new VlanMappingsDoc();
      }
      doc.mappings = mappings;
      await doc.save();
    } catch (error: unknown) {
      logger.log('error', `Failed to save VLAN mappings to database: ${(error as Error).message}`);
    }
  }
}
