/**
 * IAuth Daemon - Main implementation
 * Handles communication with Nefarious IRCd and DNSBL lookups
 */

import { createInterface, Interface } from 'node:readline';
import { readConfigFile } from './config.js';
import {
  reverseIPv4,
  isIPv4,
  getCacheEntry,
  setCacheEntry,
  getCacheSize,
  lookupDNSBL,
  matchesDNSBL,
} from './dnsbl.js';
import {
  decodeSASLPlain,
  getSupportedMechanisms,
} from './sasl.js';
import { AuthManager } from './auth/index.js';
import type { Config, ClientState, DNSBLConfig, CLIOptions, SASLState } from './types.js';

const VERSION = '1.0.0';

export class IAuthDaemon {
  private config: Config;
  private configPath: string;
  private options: CLIOptions;
  private clients = new Map<number, ClientState>();
  private dnsblCounters = new Map<number, number>();
  private countPass = 0;
  private countReject = 0;
  private countSaslSuccess = 0;
  private countSaslFail = 0;
  private startTime = Date.now();
  private rl: Interface | null = null;
  private authManager: AuthManager | null = null;
  private authInitialized = false;

  constructor(options: CLIOptions) {
    this.options = options;
    this.configPath = options.config;
    const { config } = readConfigFile(options.config);
    this.config = config;

    // Initialize DNSBL counters
    for (const dnsbl of this.config.dnsbls) {
      this.dnsblCounters.set(dnsbl.cfgNum, 0);
    }

    // Initialize auth manager asynchronously
    this.initializeAuth();
  }

  /**
   * Initialize the authentication manager
   */
  private async initializeAuth(): Promise<void> {
    if (this.config.authProviders.length > 0) {
      this.authManager = new AuthManager(this.config.authProviders);
      try {
        await this.authManager.initialize();
        this.authInitialized = true;
        const info = this.authManager.getProviderInfo();
        this.debug(`Initialized ${info.length} auth provider(s): ${info.map(p => p.name).join(', ')}`);
      } catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        this.debug(`Failed to initialize auth manager: ${message}`);
      }
    }
  }

  /**
   * Check if SASL/auth is enabled
   */
  private saslEnabled(): boolean {
    return this.authManager !== null && this.authManager.hasProviders();
  }

  /**
   * Send a message to ircd via stdout
   */
  private send(message: string): void {
    console.log(message);
  }

  /**
   * Send debug message (prefixed with "> :")
   */
  private debug(message: string): void {
    if (this.options.verbose || this.options.debug) {
      this.send(`> :${message}`);
    }
  }

  /**
   * Start the daemon
   */
  start(): void {
    this.handleStartup();

    this.rl = createInterface({
      input: process.stdin,
      output: process.stdout,
      terminal: false,
    });

    this.rl.on('line', (line) => this.handleLine(line));
    this.rl.on('close', () => {
      this.debug('STDIN closed. Shutting down...');
      process.exit(0);
    });
  }

  /**
   * Send startup messages to ircd
   */
  private handleStartup(): void {
    // Request iauth protocol version
    this.send('G 1');

    // Send version
    this.send(`V :Nefarious2 iauthd-ts version ${VERSION}`);

    // Set policy options
    this.send(`O ${this.config.policy}`);

    // Send configuration
    this.sendNewConfig();

    this.debug('Starting up');
    this.sendStats();
  }

  /**
   * Send configuration info to ircd
   */
  private sendNewConfig(): void {
    this.send('a');
    this.send(`A * version :Nefarious iauthd-ts ${VERSION}`);

    const { configLines } = readConfigFile(this.configPath);
    for (const line of configLines) {
      this.send(`A * iauthd-ts :${line}`);
    }
  }

  /**
   * Send statistics to ircd
   */
  private sendStats(): void {
    const uptime = this.formatDuration(Date.now() - this.startTime);
    const upSince = new Date(this.startTime).toUTCString();

    this.send('s');
    this.send(`S iauthd-ts :Up since ${upSince} (${uptime})`);
    this.send(`S iauthd-ts :Cache size: ${getCacheSize()}`);
    this.send(`S iauthd-ts :Total Passed: ${this.countPass}`);
    this.send(`S iauthd-ts :Total Rejected: ${this.countReject}`);

    if (this.saslEnabled()) {
      this.send(`S iauthd-ts :SASL Success: ${this.countSaslSuccess}`);
      this.send(`S iauthd-ts :SASL Failed: ${this.countSaslFail}`);
      const providerInfo = this.authManager!.getProviderInfo();
      for (const p of providerInfo) {
        this.send(`S iauthd-ts :Auth Provider: ${p.name} (priority=${p.priority}, healthy=${p.healthy})`);
      }
    }

    for (const dnsbl of this.config.dnsbls) {
      let desc = dnsbl.server;
      if (dnsbl.index) desc += ` (${dnsbl.index})`;
      if (dnsbl.bitmask) desc += ` (${dnsbl.bitmask})`;
      const count = this.dnsblCounters.get(dnsbl.cfgNum) || 0;
      this.send(`S iauthd-ts :${desc}: ${count}`);
    }
  }

  /**
   * Format duration in human-readable form
   */
  private formatDuration(ms: number): string {
    const seconds = Math.floor(ms / 1000);
    const minutes = Math.floor(seconds / 60);
    const hours = Math.floor(minutes / 60);
    const days = Math.floor(hours / 24);

    const parts: string[] = [];
    if (days > 0) parts.push(`${days}d`);
    if (hours % 24 > 0) parts.push(`${hours % 24}h`);
    if (minutes % 60 > 0) parts.push(`${minutes % 60}m`);
    if (seconds % 60 > 0 || parts.length === 0) parts.push(`${seconds % 60}s`);

    return parts.join(' ');
  }

  /**
   * Handle a line from ircd
   * Format: <id> <message> <args...>
   * Example: 26 C 172.19.0.1 12345 172.19.0.2 6667
   */
  private handleLine(line: string): void {
    if (!line) return;

    const parts = line.split(' ');
    const source = parseInt(parts[0], 10);
    const message = parts[1];
    const args = parts.slice(2).join(' ');

    if (!message) return;

    switch (message) {
      case 'C': // Client introduction
        this.handleClientIntro(source, args);
        break;

      case 'D': // Client disconnect
        this.debug(`Client ${source} disconnected.`);
        this.deleteClient(source);
        break;

      case 'F': // SSL certificate fingerprint
        // Could be used for certificate-based auth
        break;

      case 'R': // Client authenticated via SASL/LOC
        this.handleAuth(source, args);
        break;

      case 'N': // Hostname received
        break;

      case 'd': // Hostname timeout
        break;

      case 'P': // Client password
        break;

      case 'U': // Client username (full)
        break;

      case 'u': // Client username (short)
        break;

      case 'n': // Client nickname
        break;

      case 'H': // Hurry up
        this.handleHurry(source, args);
        break;

      case 'T': // Client registered
        break;

      case 'E': // Error
        this.debug(`ircd error: ${args}`);
        break;

      case 'e': // Event
        if (args === 'rehash') {
          this.debug('Got a rehash. Rereading config file');
          const { config } = readConfigFile(this.configPath);
          this.config = config;
          // Reinitialize auth providers
          if (this.authManager) {
            this.authManager.reload().catch(err => {
              this.debug(`Failed to reload auth manager: ${err}`);
            });
          } else {
            this.initializeAuth();
          }
          this.sendNewConfig();
        }
        break;

      case 'A': // SASL authentication start
        this.handleSASLStart(source, args);
        break;

      case 'a': // SASL authentication continuation
        this.handleSASLContinue(source, args);
        break;

      case 'M': // Server name and capacity
        break;

      case 'X': // Extension query reply
        break;

      case 'x': // Extension query server not linked
        break;

      case 'W': // Untrusted WEBIRC
        this.debug('Got an untrusted WEBIRC attempt. Ignoring.');
        break;

      case 'w': // Trusted WEBIRC
        this.handleWebIRC(source, args);
        break;

      default:
        this.debug(`Got unknown message '${message}' from server`);
    }
  }

  /**
   * Handle client introduction (C message)
   */
  private handleClientIntro(id: number, args: string): void {
    const [ip, portStr, serverIp, serverPortStr] = args.split(' ');
    const port = parseInt(portStr, 10);
    const serverPort = parseInt(serverPortStr, 10);

    if (!ip) {
      this.debug('Got a C without a valid IP. Ignoring');
      return;
    }

    if (this.clients.has(id)) {
      this.debug(`ERROR: Found existing entry for client ${id} (ip=${ip}). Exiting..`);
      process.exit(1);
    }

    this.debug(`Adding new entry for client ${id} (ip=${ip})`);

    const client: ClientState = {
      id,
      ip,
      port,
      serverIp,
      serverPort,
      whitelist: false,
      block: false,
      marks: new Map(),
      hurry: false,
      lookups: new Map(),
      hits: new Map(),
    };

    this.clients.set(id, client);
    this.startDNSBLLookups(client);
  }

  /**
   * Start DNSBL lookups for a client
   */
  private async startDNSBLLookups(client: ClientState): Promise<void> {
    if (!isIPv4(client.ip)) {
      this.debug(`Unknown IP format: ${client.ip}, probably IPv6... skipping DNSBL`);
      // IPv6 clients skip DNSBL but still need to be processed
      // They'll be accepted when Hurry arrives since they have no pending lookups
      return;
    }

    const reversedIp = reverseIPv4(client.ip);
    if (!reversedIp) return;

    // Start all lookups concurrently
    const lookupPromises = this.config.dnsbls.map(async (dnsbl) => {
      const query = `${reversedIp}.${dnsbl.server}`;
      client.lookups.set(dnsbl.cfgNum, true); // Mark as pending

      this.debug(`Looking up client ${client.id}: ${query}`);

      // Check cache
      const cacheTime = dnsbl.cacheTime ?? this.config.cacheTime;
      const cached = getCacheEntry(query, cacheTime);

      let results: string[];
      if (cached && cached.result !== null) {
        this.debug(`Found dnsbl cache entry for ${query}`);
        results = cached.result;
      } else if (cached) {
        // Cache entry exists but result is null (pending)
        this.debug(`Cache pending... on ${query}`);
        // Wait a bit and retry from cache
        await new Promise((resolve) => setTimeout(resolve, 100));
        const retryCache = getCacheEntry(query, cacheTime);
        results = retryCache?.result ?? [];
      } else {
        // Start new lookup
        this.debug(`Starting DNS lookup for ${query}`);
        setCacheEntry(query, null); // Mark as pending
        results = await lookupDNSBL(reversedIp, dnsbl.server, this.config.dnsTimeout);
        setCacheEntry(query, results);
      }

      // Process results
      this.handleDNSBLResponse(client, dnsbl, results);

      // Mark lookup as complete
      client.lookups.set(dnsbl.cfgNum, false);

      // Check if we should process the client now
      this.handleClientUpdate(client);
    });

    // Don't await - let lookups happen in background
    Promise.all(lookupPromises).catch((err) => {
      this.debug(`DNSBL lookup error: ${err}`);
    });
  }

  /**
   * Handle a DNSBL response
   */
  private handleDNSBLResponse(client: ClientState, dnsbl: DNSBLConfig, results: string[]): void {
    if (results.length === 0) return;

    const matched = matchesDNSBL(results, dnsbl);
    if (!matched) return;

    this.debug(`client ${client.id} matches ${dnsbl.server} result ${results.join(',')}`);

    // Apply whitelist
    if (dnsbl.whitelist) {
      client.whitelist = true;
    }

    // Apply block
    if (dnsbl.block) {
      client.block = dnsbl.block;
    }

    // Apply class
    if (dnsbl.class) {
      client.class = dnsbl.class;
    }

    // Apply mark
    if (dnsbl.mark) {
      client.marks.set(dnsbl.mark, dnsbl);
    }

    // Record hit
    client.hits.set(dnsbl.cfgNum, true);
  }

  /**
   * Check if client processing is complete
   */
  private handleClientUpdate(client: ClientState): void {
    // Count pending lookups
    let pending = 0;
    for (const isPending of client.lookups.values()) {
      if (isPending) pending++;
    }

    if (client.hurry) {
      this.debug(`Client ${client.id} has Hurry set and ${pending} pending requests`);

      if (pending === 0) {
        // Update counters for hits
        for (const cfgNum of client.hits.keys()) {
          const current = this.dnsblCounters.get(cfgNum) || 0;
          this.dnsblCounters.set(cfgNum, current + 1);
        }
        client.hits.clear();

        // Make decision
        if (client.whitelist) {
          this.clientPass(client);
        } else if (
          client.block === 'all' ||
          (client.block === 'anonymous' && !client.account)
        ) {
          this.clientReject(client, this.config.blockMsg);
        } else {
          this.clientPass(client);
        }
      }
    } else {
      this.debug(`Client ${client.id} has ${pending} pending requests`);
    }
  }

  /**
   * Handle Hurry message
   */
  private handleHurry(id: number, classArg: string): void {
    const client = this.clients.get(id);

    if (!client) {
      this.debug('ERROR: Got a hurry for a client we aren\'t even holding on to!');
      return;
    }

    this.debug(`Handling a hurry on ${id}`);
    client.hurry = true;
    this.handleClientUpdate(client);
  }

  /**
   * Handle authentication (R message)
   */
  private handleAuth(id: number, account: string): void {
    const client = this.clients.get(id);
    if (!client) return;

    this.debug(`Client ${id} authed as ${account}`);
    client.account = account;
    this.handleClientUpdate(client);
  }

  /**
   * Handle trusted WEBIRC
   */
  private handleWebIRC(id: number, args: string): void {
    const parts = args.split(' ');
    const [password, username, hostname, newIp] = parts;

    this.debug(`Got a w line: ${id} - pass=<notshown>, user=${username}, host=${hostname}, ip=${newIp}`);

    const client = this.clients.get(id);
    if (!client) {
      this.debug('Got a webirc for a client we don\'t know about? Ignored.');
      return;
    }

    // Save state
    const wasHurry = client.hurry;

    // Delete and recreate with new IP
    this.clients.delete(id);

    const newClient: ClientState = {
      id,
      ip: newIp,
      port: client.port,
      serverIp: client.serverIp,
      serverPort: client.serverPort,
      whitelist: false,
      block: false,
      marks: new Map(),
      hurry: wasHurry,
      lookups: new Map(),
      hits: new Map(),
    };

    this.clients.set(id, newClient);
    this.startDNSBLLookups(newClient);
  }

  /**
   * Accept a client
   */
  private clientPass(client: ClientState): void {
    this.debug(`Passing client ${client.id} (${client.ip})`);

    // Send marks
    for (const mark of client.marks.keys()) {
      this.sendMark(client, 'MARK', mark);
    }

    // Send done
    this.sendDone(client);
    this.countPass++;
    this.deleteClient(client.id);
    this.sendStats();
  }

  /**
   * Reject a client
   */
  private clientReject(client: ClientState, reason: string): void {
    this.debug(`Rejecting client ${client.id} (${client.ip}): ${reason}`);
    this.sendKill(client, reason);
    this.countReject++;
    this.deleteClient(client.id);
    this.sendStats();
  }

  /**
   * Delete a client from tracking
   */
  private deleteClient(id: number): void {
    this.debug('Deleting client from hash tables');
    this.clients.delete(id);
  }

  /**
   * Send mark message
   */
  private sendMark(client: ClientState, markType: string, markData: string): void {
    if (!markData) return;
    this.send(`m ${client.id} ${client.ip} ${client.port} ${markType} ${markData}`);
  }

  /**
   * Send done message
   */
  private sendDone(client: ClientState): void {
    if (client.class) {
      this.send(`D ${client.id} ${client.ip} ${client.port} ${client.class}`);
    } else {
      this.send(`D ${client.id} ${client.ip} ${client.port}`);
    }
  }

  /**
   * Send kill message
   */
  private sendKill(client: ClientState, reason: string): void {
    this.send(`k ${client.id} ${client.ip} ${client.port} :${reason}`);
  }

  // ==================== SASL Authentication ====================

  /**
   * Handle SASL authentication start (A message from IRCd)
   * Format from IRCd: "A S :<mechanism>" or "A S <mechanism> :<certfp>"
   * Or for host info: "A H :<user@host:ip>"
   * After handleLine parsing, args = "S :PLAIN" or "H :user@host:ip"
   */
  private handleSASLStart(id: number, args: string): void {
    if (!this.saslEnabled()) {
      this.debug(`SASL not enabled, ignoring A message for ${id}`);
      this.sendSASLFail(id);
      return;
    }

    // Get or create client state
    let client = this.clients.get(id);
    if (!client) {
      // Create minimal client state for SASL-only handling
      // We don't have IP/port from this message, use placeholders
      client = {
        id,
        ip: '0.0.0.0',
        port: 0,
        serverIp: '',
        serverPort: 0,
        whitelist: false,
        block: false,
        marks: new Map(),
        hurry: false,
        lookups: new Map(),
        hits: new Map(),
        sasl: { started: false },
      };
      this.clients.set(id, client);
    }

    if (!client.sasl) {
      client.sasl = { started: false };
    }

    // Parse: first character is type (S or H), rest is data
    // Format: "S :PLAIN" or "S PLAIN :certfp" or "H :user@host:ip"
    const spaceIdx = args.indexOf(' ');
    if (spaceIdx === -1) {
      this.debug(`Invalid SASL A message format: ${args}`);
      return;
    }

    const msgType = args.substring(0, spaceIdx);
    const rest = args.substring(spaceIdx + 1);

    // Check if this is host info (H) or mechanism start (S)
    if (msgType === 'H') {
      // Host info: H :<user@host:ip>
      const colonIdx = rest.indexOf(':');
      if (colonIdx !== -1) {
        client.sasl.hostInfo = rest.substring(colonIdx + 1);
        this.debug(`SASL host info for ${id}: ${client.sasl.hostInfo}`);
      }
      return;
    }

    if (msgType === 'S') {
      // Mechanism start: S :<mechanism> or S <mechanism> :<certfp>
      let mechanism: string;
      let certfp: string | undefined;

      if (rest.startsWith(':')) {
        // Format: S :PLAIN
        mechanism = rest.substring(1).trim();
      } else {
        // Format: S PLAIN :certfp
        const colonIdx = rest.indexOf(':');
        if (colonIdx !== -1) {
          mechanism = rest.substring(0, colonIdx).trim();
          certfp = rest.substring(colonIdx + 1).trim();
        } else {
          mechanism = rest.trim();
        }
      }

      this.debug(`SASL start for ${id}: mechanism=${mechanism}, certfp=${certfp || 'none'}`);

      client.sasl.mechanism = mechanism;
      client.sasl.certfp = certfp;
      client.sasl.started = true;

      // Check if mechanism is supported
      const supported = getSupportedMechanisms();
      if (!supported.includes(mechanism.toUpperCase())) {
        this.debug(`Unsupported SASL mechanism: ${mechanism}`);
        this.sendSASLMechs(id, supported);
        return;
      }

      // For PLAIN mechanism, send empty challenge to request credentials
      if (mechanism.toUpperCase() === 'PLAIN') {
        this.sendSASLChallenge(id, '+');
      }
      return;
    }

    this.debug(`Unknown SASL A message type: ${msgType}`);
  }

  /**
   * Handle SASL authentication continuation (a message from IRCd)
   * Format from IRCd: "a :<base64_data>"
   * After handleLine parsing, args = ":<base64_data>"
   */
  private handleSASLContinue(id: number, args: string): void {
    if (!this.saslEnabled()) {
      this.sendSASLFail(id);
      return;
    }

    const client = this.clients.get(id);
    if (!client || !client.sasl) {
      this.debug(`SASL continue for unknown client ${id}`);
      this.sendSASLFail(id);
      return;
    }

    // Parse: :<data>
    const colonIdx = args.indexOf(':');
    if (colonIdx === -1) {
      this.debug(`Invalid SASL a message format: ${args}`);
      this.sendSASLFail(id);
      return;
    }

    const data = args.substring(colonIdx + 1);

    if (client.sasl.mechanism?.toUpperCase() === 'PLAIN') {
      this.handleSASLPlain(client, data).catch(err => {
        this.debug(`SASL PLAIN auth error: ${err}`);
        this.sendSASLFail(id);
        this.countSaslFail++;
      });
    } else {
      this.debug(`Unhandled SASL mechanism: ${client.sasl.mechanism}`);
      this.sendSASLFail(id);
    }
  }

  /**
   * Handle SASL PLAIN authentication
   */
  private async handleSASLPlain(client: ClientState, base64Data: string): Promise<void> {
    const decoded = decodeSASLPlain(base64Data);

    if (!decoded) {
      this.debug(`Failed to decode SASL PLAIN data for ${client.id}`);
      this.sendSASLFail(client.id);
      this.countSaslFail++;
      return;
    }

    this.debug(`SASL PLAIN auth attempt for ${client.id}: authcid=${decoded.authcid}`);

    const result = await this.authManager!.authenticate(
      decoded.authcid,
      decoded.password,
      decoded.authzid
    );

    if (result.success && result.account) {
      this.debug(`SASL PLAIN auth success for ${client.id}: account=${result.account}`);
      client.account = result.account;
      this.sendSASLSuccess(client.id, result.account);
      this.countSaslSuccess++;
    } else {
      this.debug(`SASL PLAIN auth failed for ${client.id}: ${result.error || 'unknown'}`);
      this.sendSASLFail(client.id);
      this.countSaslFail++;
    }
  }

  /**
   * Send SASL challenge to client
   * Format: c <id> <ip> <port> :<challenge>
   */
  private sendSASLChallenge(id: number, challenge: string): void {
    const client = this.clients.get(id);
    if (client) {
      this.send(`c ${id} ${client.ip} ${client.port} :${challenge}`);
    }
  }

  /**
   * Send SASL login success
   * Format: L <id> <ip> <port> <account>
   */
  private sendSASLSuccess(id: number, account: string): void {
    const client = this.clients.get(id);
    if (client) {
      this.send(`L ${id} ${client.ip} ${client.port} ${account}`);
      this.send(`Z ${id} ${client.ip} ${client.port}`);
    }
  }

  /**
   * Send SASL authentication failed
   * Format: f <id> <ip> <port>
   */
  private sendSASLFail(id: number): void {
    const client = this.clients.get(id);
    if (client) {
      this.send(`f ${id} ${client.ip} ${client.port}`);
    } else {
      // Client may not exist yet, send with dummy values
      this.send(`f ${id} 0.0.0.0 0`);
    }
  }

  /**
   * Send SASL available mechanisms
   * Format: l <id> <ip> <port> :<mechanisms>
   */
  private sendSASLMechs(id: number, mechanisms: string[]): void {
    const client = this.clients.get(id);
    if (client) {
      this.send(`l ${id} ${client.ip} ${client.port} :${mechanisms.join(',')}`);
    }
  }
}
