import fs from 'node:fs';
import path from 'node:path';
import readline from 'node:readline';
import type { LogRecord } from '@zyphon/shared-types';
import { globalRedactor } from './redactor.js';

export interface DailyLogWriterOptions {
  logsDir: string;
  retentionDays?: number;
  clock?: () => Date;
  storeBodies?: boolean;
}

export function formatUtcDay(date: Date): string {
  return date.toISOString().slice(0, 10);
}

export class DailyLogWriter {
  private logsDir: string;
  private retentionDays: number;
  private clock: () => Date;
  private storeBodies: boolean;
  private currentDay: string | null = null;
  private currentFilePath: string | null = null;

  constructor(options: DailyLogWriterOptions) {
    this.logsDir = options.logsDir;
    this.retentionDays = options.retentionDays ?? 7;
    this.clock = options.clock ?? (() => new Date());
    this.storeBodies = options.storeBodies ?? false;

    if (!fs.existsSync(this.logsDir)) {
      fs.mkdirSync(this.logsDir, { recursive: true });
    }
  }

  private resolveActiveFile(): string {
    const today = formatUtcDay(this.clock());
    if (today !== this.currentDay || !this.currentFilePath) {
      this.currentDay = today;
      this.currentFilePath = path.join(this.logsDir, `zyphon-${today}.log`);
    }
    return this.currentFilePath;
  }

  /**
   * Append a single structured log record. Rolls file automatically at midnight UTC.
   */
  public write(record: LogRecord): void {
    const filePath = this.resolveActiveFile();

    // Redact secret values before disk write
    const sanitized = globalRedactor.redact(record);

    // If storeBodies is false, strip any body content
    if (!this.storeBodies && 'body_stored' in sanitized) {
      delete sanitized.body_stored;
    }

    const line = JSON.stringify(sanitized) + '\n';

    // Append to file with mode 0640
    try {
      fs.appendFileSync(filePath, line, { mode: 0o640, encoding: 'utf8' });
    } catch {
      // In case directory disappeared or permissions issue, recreate and write
      if (!fs.existsSync(this.logsDir)) {
        fs.mkdirSync(this.logsDir, { recursive: true });
      }
      fs.appendFileSync(filePath, line, { mode: 0o640, encoding: 'utf8' });
    }
  }

  /**
   * Prune log files older than retention window.
   * Keeps the newest N files matching ^zyphon-\d{4}-\d{2}-\d{2}\.log$.
   * Returns list of deleted file basenames.
   */
  /** Update retention window without restart (takes effect on next prune). */
  public setRetentionDays(days: number): void {
    if (days < 1) throw new Error('retentionDays must be >= 1');
    this.retentionDays = days;
  }

  public getRetentionDays(): number {
    return this.retentionDays;
  }

  public prune(retentionDays = this.retentionDays): string[] {
    if (!fs.existsSync(this.logsDir)) return [];

    const files = fs
      .readdirSync(this.logsDir)
      .filter((file) => /^zyphon-\d{4}-\d{2}-\d{2}\.log$/.test(file))
      .sort(); // Lexicographical sort corresponds to chronological sort for YYYY-MM-DD

    const deleted: string[] = [];
    while (files.length > retentionDays) {
      const oldest = files.shift();
      if (oldest) {
        const fullPath = path.join(this.logsDir, oldest);
        try {
          fs.unlinkSync(fullPath);
          deleted.push(oldest);
        } catch {
          // ignore unlink error
        }
      }
    }

    return deleted;
  }

  /**
   * List all currently existing daily log files.
   */
  public listLogFiles(): Array<{ filename: string; day: string; sizeBytes: number; lineCount?: number }> {
    if (!fs.existsSync(this.logsDir)) return [];

    return fs
      .readdirSync(this.logsDir)
      .filter((file) => /^zyphon-\d{4}-\d{2}-\d{2}\.log$/.test(file))
      .sort()
      .reverse()
      .map((filename) => {
        const fullPath = path.join(this.logsDir, filename);
        const stats = fs.statSync(fullPath);
        const day = filename.replace(/^zyphon-/, '').replace(/\.log$/, '');
        return {
          filename,
          day,
          sizeBytes: stats.size,
        };
      });
  }
}

export interface LogFilterOptions {
  query?: string;
  status?: string;
  application?: string;
  channel?: string;
  provider?: string;
  startTime?: number;
  endTime?: number;
  limit?: number;
  cursor?: string; // offset or message_id
}

export interface LogQueryResult {
  records: LogRecord[];
  nextCursor?: string;
  totalScanned: number;
}

export class DailyLogReader {
  private logsDir: string;

  constructor(logsDir: string) {
    this.logsDir = logsDir;
  }

  /**
   * Search and filter daily logs newest-first with streaming readline.
   */
  public async query(options: LogFilterOptions): Promise<LogQueryResult> {
    if (!fs.existsSync(this.logsDir)) {
      return { records: [], totalScanned: 0 };
    }

    const files = fs
      .readdirSync(this.logsDir)
      .filter((file) => /^zyphon-\d{4}-\d{2}-\d{2}\.log$/.test(file))
      .sort()
      .reverse(); // Newest first

    const limit = Math.min(options.limit ?? 50, 500);
    const results: LogRecord[] = [];
    let totalScanned = 0;
    let skipping = options.cursor ? true : false;
    let nextCursor: string | undefined;

    const lowerQuery = options.query?.toLowerCase();

    for (const file of files) {
      if (results.length >= limit) break;

      const fullPath = path.join(this.logsDir, file);
      const fileStream = fs.createReadStream(fullPath, { encoding: 'utf8' });
      const rl = readline.createInterface({
        input: fileStream,
        crlfDelay: Infinity,
      });

      // Daily files are written chronologically; read all lines into an array and reverse for newest-first order
      const lines: string[] = [];
      for await (const line of rl) {
        if (line.trim()) lines.push(line);
      }

      for (let i = lines.length - 1; i >= 0; i--) {
        const line = lines[i];
        if (!line) continue;
        totalScanned++;

        let rec: LogRecord;
        try {
          rec = JSON.parse(line);
        } catch {
          continue;
        }

        // Handle cursor skipping
        if (skipping) {
          if ('message_id' in rec && rec.message_id === options.cursor) {
            skipping = false; // Next matching item will be collected
          } else if ('msgId' in rec && rec.msgId === options.cursor) {
            skipping = false;
          }
          continue;
        }

        // Apply filters
        if (options.startTime && 'ts' in rec && rec.ts < options.startTime) continue;
        if (options.endTime && 'ts' in rec && rec.ts > options.endTime) continue;

        if (options.status) {
          if ('status' in rec && rec.status !== options.status) continue;
        }

        if (options.application) {
          if ('application' in rec && rec.application !== options.application) continue;
        }

        if (options.channel) {
          if ('channel' in rec && rec.channel !== options.channel) continue;
        }

        if (options.provider) {
          if ('provider' in rec && rec.provider !== options.provider) continue;
        }

        if (lowerQuery) {
          const rawStr = JSON.stringify(rec).toLowerCase();
          if (!rawStr.includes(lowerQuery)) continue;
        }

        results.push(rec);

        if (results.length >= limit) {
          if ('message_id' in rec) nextCursor = rec.message_id;
          else if ('msgId' in rec) nextCursor = rec.msgId;
          break;
        }
      }
    }

    return {
      records: results,
      nextCursor,
      totalScanned,
    };
  }

  /**
   * Find a specific message and its delivery updates across all daily logs.
   */
  public async getMessageTimeline(messageId: string): Promise<LogRecord[]> {
    if (!fs.existsSync(this.logsDir)) return [];

    const files = fs
      .readdirSync(this.logsDir)
      .filter((file) => /^zyphon-\d{4}-\d{2}-\d{2}\.log$/.test(file))
      .sort()
      .reverse();

    const matched: LogRecord[] = [];

    for (const file of files) {
      const fullPath = path.join(this.logsDir, file);
      const fileStream = fs.createReadStream(fullPath, { encoding: 'utf8' });
      const rl = readline.createInterface({
        input: fileStream,
        crlfDelay: Infinity,
      });

      for await (const line of rl) {
        if (!line.includes(messageId)) continue;
        try {
          const rec: LogRecord = JSON.parse(line);
          if (('message_id' in rec && rec.message_id === messageId) || ('msgId' in rec && rec.msgId === messageId)) {
            matched.push(rec);
          }
        } catch {
          // ignore
        }
      }
    }

    return matched.sort((a, b) => a.ts - b.ts);
  }

  /**
   * Read raw contents and parsed records of a specific daily log file.
   */
  public readLogFile(filename: string): { filename: string; raw: string; records: LogRecord[] } | null {
    if (!/^zyphon-\d{4}-\d{2}-\d{2}\.log$/.test(filename)) {
      return null;
    }
    const fullPath = path.join(this.logsDir, filename);
    if (!fs.existsSync(fullPath)) {
      return null;
    }
    const raw = fs.readFileSync(fullPath, 'utf8');
    const records: LogRecord[] = [];
    const lines = raw.split('\n');
    for (let i = lines.length - 1; i >= 0; i--) {
      const line = lines[i]?.trim();
      if (!line) continue;
      try {
        records.push(JSON.parse(line));
      } catch {
        // ignore malformed line
      }
    }
    return { filename, raw, records };
  }
}

