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

/**
 * RADIUS accounting session
 */
export interface IAccountingSession {
  /** Unique session ID from RADIUS */
  sessionId: string;
  /** Username (often MAC address for MAB) */
  username: string;
  /** MAC address of the device */
  macAddress?: string;
  /** NAS IP address (switch/AP) */
  nasIpAddress: string;
  /** NAS port (physical or virtual) */
  nasPort?: number;
  /** NAS port type */
  nasPortType?: string;
  /** NAS identifier (name) */
  nasIdentifier?: string;
  /** Assigned VLAN */
  vlanId?: number;
  /** Assigned IP address (if any) */
  framedIpAddress?: string;
  /** Called station ID (usually BSSID for wireless) */
  calledStationId?: string;
  /** Calling station ID (usually client MAC) */
  callingStationId?: string;
  /** Session start time */
  startTime: number;
  /** Session end time (0 if active) */
  endTime: number;
  /** Last update time (interim accounting) */
  lastUpdateTime: number;
  /** Session status */
  status: 'active' | 'stopped' | 'terminated';
  /** Termination cause (if stopped) */
  terminateCause?: string;
  /** Input octets (bytes received by NAS from client) */
  inputOctets: number;
  /** Output octets (bytes sent by NAS to client) */
  outputOctets: number;
  /** Input packets */
  inputPackets: number;
  /** Output packets */
  outputPackets: number;
  /** Session duration in seconds */
  sessionTime: number;
  /** Service type */
  serviceType?: string;
}

/**
 * Accounting summary for a time period
 */
export interface IAccountingSummary {
  /** Time period start */
  periodStart: number;
  /** Time period end */
  periodEnd: number;
  /** Total sessions */
  totalSessions: number;
  /** Active sessions */
  activeSessions: number;
  /** Total input bytes */
  totalInputBytes: number;
  /** Total output bytes */
  totalOutputBytes: number;
  /** Total session time (seconds) */
  totalSessionTime: number;
  /** Average session duration (seconds) */
  averageSessionDuration: number;
  /** Unique users/devices */
  uniqueUsers: number;
  /** Sessions by VLAN */
  sessionsByVlan: Record<number, number>;
  /** Top users by traffic */
  topUsersByTraffic: Array<{ username: string; totalBytes: number }>;
}

/**
 * Accounting manager configuration
 */
export interface IAccountingManagerConfig {
  /** Session retention period in days (default: 30) */
  retentionDays?: number;
  /** Enable detailed session logging */
  detailedLogging?: boolean;
  /** Maximum active sessions to track in memory */
  maxActiveSessions?: number;
  /** Stale session timeout in hours — sessions with no update for this long are evicted (default: 24) */
  staleSessionTimeoutHours?: number;
}

/**
 * Manages RADIUS accounting data including:
 * - Session tracking (start/stop/interim)
 * - Data usage tracking (bytes in/out)
 * - Session history and retention
 * - Billing reports and summaries
 */
export class AccountingManager {
  private activeSessions: Map<string, IAccountingSession> = new Map();
  private config: Required<IAccountingManagerConfig>;
  private staleSessionSweepTimer?: ReturnType<typeof setInterval>;

  // Counters for statistics
  private stats = {
    totalSessionsStarted: 0,
    totalSessionsStopped: 0,
    totalInputBytes: 0,
    totalOutputBytes: 0,
    interimUpdatesReceived: 0,
  };

  constructor(config?: IAccountingManagerConfig) {
    this.config = {
      retentionDays: config?.retentionDays ?? 30,
      detailedLogging: config?.detailedLogging ?? false,
      maxActiveSessions: config?.maxActiveSessions ?? 10000,
      staleSessionTimeoutHours: config?.staleSessionTimeoutHours ?? 24,
    };
  }

  /**
   * Initialize the accounting manager
   */
  async initialize(): Promise<void> {
    await this.loadActiveSessions();

    // Start periodic sweep to evict stale sessions (every 15 minutes)
    this.staleSessionSweepTimer = setInterval(() => {
      this.sweepStaleSessions();
    }, 15 * 60 * 1000);
    // Allow the process to exit even if the timer is pending
    if (this.staleSessionSweepTimer.unref) {
      this.staleSessionSweepTimer.unref();
    }

    logger.log('info', `AccountingManager initialized with ${this.activeSessions.size} active sessions`);
  }

  /**
   * Stop the accounting manager and clean up timers
   */
  stop(): void {
    if (this.staleSessionSweepTimer) {
      clearInterval(this.staleSessionSweepTimer);
      this.staleSessionSweepTimer = undefined;
    }
  }

  /**
   * Sweep stale active sessions that have not received any update
   * within the configured timeout. These are orphaned sessions where
   * the Stop packet was never received.
   */
  private sweepStaleSessions(): void {
    const timeoutMs = this.config.staleSessionTimeoutHours * 60 * 60 * 1000;
    const cutoff = Date.now() - timeoutMs;
    let swept = 0;

    for (const [sessionId, session] of this.activeSessions) {
      if (session.lastUpdateTime < cutoff) {
        session.status = 'terminated';
        session.terminateCause = 'StaleSessionTimeout';
        session.endTime = Date.now();
        session.sessionTime = Math.floor((session.endTime - session.startTime) / 1000);

        this.persistSession(session).catch(() => {});

        this.activeSessions.delete(sessionId);
        swept++;
      }
    }

    if (swept > 0) {
      logger.log('info', `Swept ${swept} stale RADIUS sessions (no update for ${this.config.staleSessionTimeoutHours}h)`);
    }
  }

  /**
   * Handle accounting start request
   */
  async handleAccountingStart(data: {
    sessionId: string;
    username: string;
    macAddress?: string;
    nasIpAddress: string;
    nasPort?: number;
    nasPortType?: string;
    nasIdentifier?: string;
    vlanId?: number;
    framedIpAddress?: string;
    calledStationId?: string;
    callingStationId?: string;
    serviceType?: string;
  }): Promise<void> {
    const now = Date.now();

    const session: IAccountingSession = {
      sessionId: data.sessionId,
      username: data.username,
      macAddress: data.macAddress,
      nasIpAddress: data.nasIpAddress,
      nasPort: data.nasPort,
      nasPortType: data.nasPortType,
      nasIdentifier: data.nasIdentifier,
      vlanId: data.vlanId,
      framedIpAddress: data.framedIpAddress,
      calledStationId: data.calledStationId,
      callingStationId: data.callingStationId,
      serviceType: data.serviceType,
      startTime: now,
      endTime: 0,
      lastUpdateTime: now,
      status: 'active',
      inputOctets: 0,
      outputOctets: 0,
      inputPackets: 0,
      outputPackets: 0,
      sessionTime: 0,
    };

    // Check if we're at capacity
    if (this.activeSessions.size >= this.config.maxActiveSessions) {
      // Remove oldest session
      const oldest = this.findOldestSession();
      if (oldest) {
        await this.evictSession(oldest);
      }
    }

    this.activeSessions.set(data.sessionId, session);
    this.stats.totalSessionsStarted++;

    if (this.config.detailedLogging) {
      logger.log('info', `Accounting Start: session=${data.sessionId}, user=${data.username}, NAS=${data.nasIpAddress}`);
    }

    // Persist session
    await this.persistSession(session);
  }

  /**
   * Handle accounting interim update request
   */
  async handleAccountingUpdate(data: {
    sessionId: string;
    inputOctets?: number;
    outputOctets?: number;
    inputPackets?: number;
    outputPackets?: number;
    sessionTime?: number;
  }): Promise<void> {
    const session = this.activeSessions.get(data.sessionId);

    if (!session) {
      logger.log('warn', `Interim update for unknown session: ${data.sessionId}`);
      return;
    }

    // Update session metrics
    if (data.inputOctets !== undefined) {
      session.inputOctets = data.inputOctets;
    }
    if (data.outputOctets !== undefined) {
      session.outputOctets = data.outputOctets;
    }
    if (data.inputPackets !== undefined) {
      session.inputPackets = data.inputPackets;
    }
    if (data.outputPackets !== undefined) {
      session.outputPackets = data.outputPackets;
    }
    if (data.sessionTime !== undefined) {
      session.sessionTime = data.sessionTime;
    }

    session.lastUpdateTime = Date.now();
    this.stats.interimUpdatesReceived++;

    if (this.config.detailedLogging) {
      logger.log('debug', `Accounting Interim: session=${data.sessionId}, in=${data.inputOctets}, out=${data.outputOctets}`);
    }

    // Update persisted session
    await this.persistSession(session);
  }

  /**
   * Handle accounting stop request
   */
  async handleAccountingStop(data: {
    sessionId: string;
    terminateCause?: string;
    inputOctets?: number;
    outputOctets?: number;
    inputPackets?: number;
    outputPackets?: number;
    sessionTime?: number;
  }): Promise<void> {
    const session = this.activeSessions.get(data.sessionId);

    if (!session) {
      logger.log('warn', `Stop for unknown session: ${data.sessionId}`);
      return;
    }

    // Update final metrics
    if (data.inputOctets !== undefined) {
      session.inputOctets = data.inputOctets;
    }
    if (data.outputOctets !== undefined) {
      session.outputOctets = data.outputOctets;
    }
    if (data.inputPackets !== undefined) {
      session.inputPackets = data.inputPackets;
    }
    if (data.outputPackets !== undefined) {
      session.outputPackets = data.outputPackets;
    }
    if (data.sessionTime !== undefined) {
      session.sessionTime = data.sessionTime;
    }

    session.endTime = Date.now();
    session.lastUpdateTime = session.endTime;
    session.status = 'stopped';
    session.terminateCause = data.terminateCause;

    // Update global stats
    this.stats.totalSessionsStopped++;
    this.stats.totalInputBytes += session.inputOctets;
    this.stats.totalOutputBytes += session.outputOctets;

    if (this.config.detailedLogging) {
      logger.log('info', `Accounting Stop: session=${data.sessionId}, duration=${session.sessionTime}s, in=${session.inputOctets}, out=${session.outputOctets}`);
    }

    // Update status in the database (single collection, no active->archive move needed)
    await this.persistSession(session);

    // Remove from active sessions
    this.activeSessions.delete(data.sessionId);
  }

  /**
   * Get an active session by ID
   */
  getSession(sessionId: string): IAccountingSession | undefined {
    return this.activeSessions.get(sessionId);
  }

  /**
   * Get all active sessions
   */
  getActiveSessions(): IAccountingSession[] {
    return Array.from(this.activeSessions.values());
  }

  /**
   * Get active sessions by username
   */
  getSessionsByUsername(username: string): IAccountingSession[] {
    return Array.from(this.activeSessions.values()).filter(s => s.username === username);
  }

  /**
   * Get active sessions by NAS IP
   */
  getSessionsByNas(nasIpAddress: string): IAccountingSession[] {
    return Array.from(this.activeSessions.values()).filter(s => s.nasIpAddress === nasIpAddress);
  }

  /**
   * Get active sessions by VLAN
   */
  getSessionsByVlan(vlanId: number): IAccountingSession[] {
    return Array.from(this.activeSessions.values()).filter(s => s.vlanId === vlanId);
  }

  /**
   * Get accounting summary for a time period
   */
  async getSummary(startTime: number, endTime: number): Promise<IAccountingSummary> {
    // Get archived sessions for the time period
    const archivedSessions = await this.getArchivedSessions(startTime, endTime);

    // Combine with active sessions that started within the period
    const activeSessions = Array.from(this.activeSessions.values()).filter(
      s => s.startTime >= startTime && s.startTime <= endTime
    );

    const allSessions = [...archivedSessions, ...activeSessions];

    // Calculate summary
    let totalInputBytes = 0;
    let totalOutputBytes = 0;
    let totalSessionTime = 0;
    const uniqueUsers = new Set<string>();
    const sessionsByVlan: Record<number, number> = {};
    const userTraffic: Record<string, number> = {};

    for (const session of allSessions) {
      totalInputBytes += session.inputOctets;
      totalOutputBytes += session.outputOctets;
      totalSessionTime += session.sessionTime;
      uniqueUsers.add(session.username);

      if (session.vlanId !== undefined) {
        sessionsByVlan[session.vlanId] = (sessionsByVlan[session.vlanId] || 0) + 1;
      }

      const userBytes = session.inputOctets + session.outputOctets;
      userTraffic[session.username] = (userTraffic[session.username] || 0) + userBytes;
    }

    // Top users by traffic
    const topUsersByTraffic = Object.entries(userTraffic)
      .sort((a, b) => b[1] - a[1])
      .slice(0, 10)
      .map(([username, totalBytes]) => ({ username, totalBytes }));

    return {
      periodStart: startTime,
      periodEnd: endTime,
      totalSessions: allSessions.length,
      activeSessions: activeSessions.length,
      totalInputBytes,
      totalOutputBytes,
      totalSessionTime,
      averageSessionDuration: allSessions.length > 0 ? totalSessionTime / allSessions.length : 0,
      uniqueUsers: uniqueUsers.size,
      sessionsByVlan,
      topUsersByTraffic,
    };
  }

  /**
   * Get statistics
   */
  getStats(): {
    activeSessions: number;
    totalSessionsStarted: number;
    totalSessionsStopped: number;
    totalInputBytes: number;
    totalOutputBytes: number;
    interimUpdatesReceived: number;
  } {
    return {
      activeSessions: this.activeSessions.size,
      ...this.stats,
    };
  }

  /**
   * Disconnect a session (admin action)
   */
  async disconnectSession(sessionId: string, reason: string = 'AdminReset'): Promise<boolean> {
    const session = this.activeSessions.get(sessionId);
    if (!session) {
      return false;
    }

    await this.handleAccountingStop({
      sessionId,
      terminateCause: reason,
      sessionTime: Math.floor((Date.now() - session.startTime) / 1000),
    });

    return true;
  }

  /**
   * Clean up old archived sessions based on retention policy
   */
  async cleanupOldSessions(): Promise<number> {
    const cutoffTime = Date.now() - this.config.retentionDays * 24 * 60 * 60 * 1000;
    let deletedCount = 0;

    try {
      const oldDocs = await AccountingSessionDoc.findStoppedBefore(cutoffTime);

      for (const doc of oldDocs) {
        try {
          await doc.delete();
          deletedCount++;
        } catch (error) {
          // Ignore individual errors
        }
      }

      if (deletedCount > 0) {
        logger.log('info', `Cleaned up ${deletedCount} old accounting sessions`);
      }
    } catch (error: unknown) {
      logger.log('error', `Failed to cleanup old sessions: ${(error as Error).message}`);
    }

    return deletedCount;
  }

  /**
   * Find the oldest active session
   */
  private findOldestSession(): string | null {
    let oldestTime = Infinity;
    let oldestSessionId: string | null = null;

    for (const [sessionId, session] of this.activeSessions) {
      if (session.lastUpdateTime < oldestTime) {
        oldestTime = session.lastUpdateTime;
        oldestSessionId = sessionId;
      }
    }

    return oldestSessionId;
  }

  /**
   * Evict a session from memory
   */
  private async evictSession(sessionId: string): Promise<void> {
    const session = this.activeSessions.get(sessionId);
    if (session) {
      session.status = 'terminated';
      session.terminateCause = 'SessionEvicted';
      session.endTime = Date.now();

      await this.persistSession(session);

      this.activeSessions.delete(sessionId);
      logger.log('warn', `Evicted session ${sessionId} due to capacity limit`);
    }
  }

  /**
   * Load active sessions from database
   */
  private async loadActiveSessions(): Promise<void> {
    try {
      const docs = await AccountingSessionDoc.findActive();

      for (const doc of docs) {
        const session: IAccountingSession = {
          sessionId: doc.sessionId,
          username: doc.username,
          macAddress: doc.macAddress,
          nasIpAddress: doc.nasIpAddress,
          nasPort: doc.nasPort,
          nasPortType: doc.nasPortType,
          nasIdentifier: doc.nasIdentifier,
          vlanId: doc.vlanId,
          framedIpAddress: doc.framedIpAddress,
          calledStationId: doc.calledStationId,
          callingStationId: doc.callingStationId,
          startTime: doc.startTime,
          endTime: doc.endTime,
          lastUpdateTime: doc.lastUpdateTime,
          status: doc.status,
          terminateCause: doc.terminateCause,
          inputOctets: doc.inputOctets,
          outputOctets: doc.outputOctets,
          inputPackets: doc.inputPackets,
          outputPackets: doc.outputPackets,
          sessionTime: doc.sessionTime,
          serviceType: doc.serviceType,
        };
        this.activeSessions.set(session.sessionId, session);
      }
    } catch (error: unknown) {
      logger.log('warn', `Failed to load active sessions: ${(error as Error).message}`);
    }
  }

  /**
   * Persist a session to the database (create or update)
   */
  private async persistSession(session: IAccountingSession): Promise<void> {
    try {
      let doc = await AccountingSessionDoc.findBySessionId(session.sessionId);
      if (!doc) {
        doc = new AccountingSessionDoc();
      }
      Object.assign(doc, session);
      await doc.save();
    } catch (error: unknown) {
      logger.log('error', `Failed to persist session ${session.sessionId}: ${(error as Error).message}`);
    }
  }

  /**
   * Get archived (stopped/terminated) sessions for a time period
   */
  private async getArchivedSessions(startTime: number, endTime: number): Promise<IAccountingSession[]> {
    const sessions: IAccountingSession[] = [];

    try {
      const docs = await AccountingSessionDoc.getInstances({
        status: { $in: ['stopped', 'terminated'] } as any,
        endTime: { $gt: 0, $gte: startTime } as any,
        startTime: { $lte: endTime } as any,
      });

      for (const doc of docs) {
        sessions.push({
          sessionId: doc.sessionId,
          username: doc.username,
          macAddress: doc.macAddress,
          nasIpAddress: doc.nasIpAddress,
          nasPort: doc.nasPort,
          nasPortType: doc.nasPortType,
          nasIdentifier: doc.nasIdentifier,
          vlanId: doc.vlanId,
          framedIpAddress: doc.framedIpAddress,
          calledStationId: doc.calledStationId,
          callingStationId: doc.callingStationId,
          startTime: doc.startTime,
          endTime: doc.endTime,
          lastUpdateTime: doc.lastUpdateTime,
          status: doc.status,
          terminateCause: doc.terminateCause,
          inputOctets: doc.inputOctets,
          outputOctets: doc.outputOctets,
          inputPackets: doc.inputPackets,
          outputPackets: doc.outputPackets,
          sessionTime: doc.sessionTime,
          serviceType: doc.serviceType,
        });
      }
    } catch (error: unknown) {
      logger.log('warn', `Failed to get archived sessions: ${(error as Error).message}`);
    }

    return sessions;
  }
}
