import { appendFileSync, existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from "node:fs";
import { basename, dirname, join } from "node:path";

export const DEFAULT_LOG_RETENTION_BYTES = 1024 * 1024 * 1024;

export interface HourlyJsonlWriter {
  appendLine(line: string, at?: Date): void;
  appendJson(value: unknown, at?: Date): void;
  currentPath(at?: Date): string;
  prune(at?: Date): void;
}

interface HourlyJsonlWriterOptions {
  baseLogFile: string;
  service: string;
  maxBytes?: number;
}

interface FileEntry {
  path: string;
  size: number;
  mtimeMs: number;
}

function pad(value: number): string {
  return String(value).padStart(2, "0");
}

function localParts(date: Date): { day: string; hour: string } {
  return {
    day: `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}`,
    hour: pad(date.getHours()),
  };
}

function safeServiceName(service: string): string {
  return service.replace(/[^A-Za-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "") || "service";
}

function parseBase(baseLogFile: string, service: string): { rootDir: string; prefix: string; suffix: string } {
  const suffix = `_${service}.jsonl`;
  const dir = dirname(baseLogFile);
  const dirName = basename(dir);
  const rootDir = /^\d{8}$/u.test(dirName) ? dirname(dir) : dir;
  const fileName = basename(baseLogFile);
  const stem = fileName.replace(/\.jsonl$/u, "");
  const ownPrefix = fileName.endsWith(suffix) ? fileName.slice(0, -suffix.length) : "";
  const startStampPrefix = stem.match(/^(\d{8}_\d{6})(?:_\d{8}_\d{2})?_/u)?.[1] ?? "";
  const genericPrefix = stem.includes("_") ? stem.slice(0, stem.lastIndexOf("_")) : stem;
  const prefix = ownPrefix || startStampPrefix || genericPrefix;
  return { rootDir, prefix: prefix || "unidesk", suffix };
}

function collectFiles(root: string, suffix: string): FileEntry[] {
  if (!existsSync(root)) return [];
  const result: FileEntry[] = [];
  const scan = (dir: string): void => {
    let entries;
    try {
      entries = readdirSync(dir, { withFileTypes: true });
    } catch {
      return;
    }
    for (const entry of entries) {
      const path = join(dir, entry.name);
      if (entry.isDirectory()) {
        scan(path);
        continue;
      }
      if (!entry.isFile() || !entry.name.endsWith(suffix)) continue;
      try {
        const stat = statSync(path);
        result.push({ path, size: stat.size, mtimeMs: stat.mtimeMs });
      } catch {
        // Ignore files that disappear while retention is scanning.
      }
    }
  };
  scan(root);
  return result;
}

export function logRetentionBytesFromEnv(name: string, fallback = DEFAULT_LOG_RETENTION_BYTES): number {
  const raw = process.env[name]?.trim();
  if (!raw) return fallback;
  const match = raw.match(/^(\d+(?:\.\d+)?)\s*(b|k|kb|kib|m|mb|mib|g|gb|gib)?$/iu);
  if (!match) return fallback;
  const value = Number(match[1]);
  const unit = (match[2] ?? "b").toLowerCase();
  const multiplier =
    unit === "g" || unit === "gb" || unit === "gib" ? 1024 * 1024 * 1024
      : unit === "m" || unit === "mb" || unit === "mib" ? 1024 * 1024
        : unit === "k" || unit === "kb" || unit === "kib" ? 1024
          : 1;
  const bytes = value * multiplier;
  return Number.isFinite(bytes) && bytes > 0 ? Math.floor(bytes) : fallback;
}

export function logRetentionEnvNameForService(service: string): string {
  const normalized = safeServiceName(service).toUpperCase().replace(/[^A-Z0-9]+/g, "_");
  return `UNIDESK_${normalized}_LOG_MAX_BYTES`;
}

export function logRetentionBytesForService(service: string, extraEnvNames: string[] = [], fallback = DEFAULT_LOG_RETENTION_BYTES): number {
  const globalMaxBytes = logRetentionBytesFromEnv("UNIDESK_LOG_RETENTION_BYTES", fallback);
  for (const name of [logRetentionEnvNameForService(service), ...extraEnvNames]) {
    if (process.env[name]?.trim()) return logRetentionBytesFromEnv(name, globalMaxBytes);
  }
  return globalMaxBytes;
}

export function createHourlyJsonlWriter(options: HourlyJsonlWriterOptions): HourlyJsonlWriter {
  const service = safeServiceName(options.service);
  const maxBytes = Math.max(1, Math.floor(options.maxBytes ?? DEFAULT_LOG_RETENTION_BYTES));
  const { rootDir, prefix, suffix } = parseBase(options.baseLogFile, service);
  let lastPruneAt = 0;

  const currentPath = (at = new Date()): string => {
    const parts = localParts(at);
    return join(rootDir, parts.day, `${prefix}_${parts.day}_${parts.hour}${suffix}`);
  };

  const prune = (at = new Date()): void => {
    const activePath = currentPath(at);
    const files = collectFiles(rootDir, suffix).sort((left, right) => {
      if (left.path === activePath) return 1;
      if (right.path === activePath) return -1;
      return left.path.localeCompare(right.path) || left.mtimeMs - right.mtimeMs;
    });
    let total = files.reduce((sum, file) => sum + file.size, 0);
    for (const file of files) {
      if (total <= maxBytes) break;
      if (file.path === activePath) continue;
      try {
        unlinkSync(file.path);
        total -= file.size;
      } catch {
        // Best-effort retention must never break service logging.
      }
    }
  };

  const maybePrune = (at: Date): void => {
    const now = Date.now();
    if (now - lastPruneAt < 60_000) return;
    lastPruneAt = now;
    prune(at);
  };

  const appendLine = (line: string, at = new Date()): void => {
    const path = currentPath(at);
    mkdirSync(dirname(path), { recursive: true });
    appendFileSync(path, line.endsWith("\n") ? line : `${line}\n`, "utf8");
    maybePrune(at);
  };

  return {
    appendLine,
    appendJson(value: unknown, at = new Date()): void {
      appendLine(JSON.stringify(value), at);
    },
    currentPath,
    prune,
  };
}
