#!/usr/bin/env node

import * as fs from 'node:fs';
import * as path from 'node:path';

import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

type Config = {
  root_dirs?: string[];
  output_file?: string;
  processed_log?: string;
  extensions?: string[];
};

type Args = {
  scan: string[] | null;
  ignore: string[];
  reset: boolean;
  hilfe: boolean;
};

class ScriptInfo {
  static dir(): string {
    return path.dirname(__filename);
  }

  static cwd(): string {
    return process.cwd();
  }
}

class ConfigLoader {
  static load(filename = 'config.jsonc'): Config {
    const configPath = path.join(ScriptInfo.dir(), filename);
    if (!fs.existsSync(configPath)) {
      console.error(` ⚠  Konfigurationsdatei nicht gefunden: ${configPath}`);
      process.exit(1);
    }
    const raw = fs.readFileSync(configPath, 'utf8');
    const clean = raw
      .split('\n')
      .filter(line => !line.trim().startsWith('//'))
      .join('');
    return JSON.parse(clean);
  }
}

class ArgParser {
  static parse(): Args {
    const argv = process.argv.slice(2);
    const result: Args = { scan: null, ignore: [], reset: false, hilfe: false };

    while (argv.length) {
      const arg = argv.shift();
      if (!arg) break;

      if (arg === '-h' || arg === '--hilfe') {
        result.hilfe = true;
      } else if (arg === '--reset') {
        result.reset = true;
      } else if ((arg === '-s' || arg === '--scan') && argv.length) {
        result.scan = argv.shift()!.split(',').map(x => x.trim()).filter(Boolean);
      } else if ((arg === '-x' || arg === '--ignore') && argv.length) {
        result.ignore = argv.shift()!.split(',').map(x => x.trim()).filter(Boolean);
      } else {
        console.warn(` ⚠  Unbekannter Parameter: ${arg}`);
        result.hilfe = true;
        break;
      }
    }

    return result;
  }

  static showHelp(): void {
    console.log(`
(C) 2025 - Adam Skotarczak (ionivation.com)

🛈 Tool das Verzeichnisse nach Markdown-Dateien durchsucht und diese an eine definierte Liste anhängt als Markdown-Link.
Verwendung: ts-node link_collector.ts [OPTIONEN]

  -s, --scan    Kommagetrennte Liste von Verzeichnissen zum Durchsuchen (relativ zum Aufrufpfad)
  -x, --ignore  Kommagetrennte Liste von Verzeichnissen, die ignoriert werden sollen
  --reset       Löscht das Logfile 'processed.log' und beendet sich
  -h, --hilfe   Zeigt diese Hilfe

Beispiel:
  ts-node link_collector.ts -s docs,notes -x "docs/alt"
`);
  }
}

class FileScanner {
  static *find(rootDirs: string[], ignoreDirs: string[], extensions: string[]): Generator<string> {
    for (const root of rootDirs) {
      const absRoot = path.resolve(root);
      const stack = [absRoot];

      while (stack.length) {
        const current = stack.pop()!;
        const rel = path.relative(ScriptInfo.cwd(), current).replace(/\\/g, '/');
        if (ignoreDirs.some(ignored => rel.startsWith(ignored))) continue;

        const entries = fs.readdirSync(current, { withFileTypes: true });
        for (const entry of entries) {
          const fullPath = path.join(current, entry.name);
          if (entry.isDirectory()) {
            stack.push(fullPath);
          } else if (extensions.some(ext => entry.name.endsWith(ext))) {
            yield fullPath;
          }
        }
      }
    }
  }
}

class TitleExtractor {
  static extract(filepath: string): string {
    try {
      const content = fs.readFileSync(filepath, 'utf8');
      for (const line of content.split('\n')) {
        if (line.trim().startsWith('#')) {
          return line.replace(/^#+/, '').trim();
        }
      }
    } catch (e) {
      console.error(`⚠ Fehler beim Lesen von ${filepath}: ${e}`);
    }
    return path.basename(filepath, path.extname(filepath));
  }
}

class ProcessedLog {
  private readonly logPath: string;
  private entries: Set<string>;

  constructor(filename: string) {
    this.logPath = path.join(ScriptInfo.dir(), filename);
    this.entries = new Set<string>();
    this.load();
  }

  private load() {
    if (!fs.existsSync(this.logPath)) return;
    const lines = fs.readFileSync(this.logPath, 'utf8').split('\n').map(x => x.trim()).filter(Boolean);
    this.entries = new Set(lines.map(p => p.replace(/\\/g, '/')));
  }

  has(posixPath: string): boolean {
    return this.entries.has(posixPath);
  }

  update(newPaths: string[]): void {
    const content = newPaths.map(p => p.replace(/\\/g, '/')).join('\n') + '\n';
    fs.appendFileSync(this.logPath, content, 'utf8');
  }

  reset(): void {
    if (fs.existsSync(this.logPath)) {
      fs.unlinkSync(this.logPath);
      console.log('🧹  Logfile gelöscht:', this.logPath);
    } else {
      console.log('ℹ Logfile existierte nicht:', this.logPath);
    }
  }
}

class MarkdownAppender {
  static append(outputFile: string, links: string[]): void {
    const content = links.join('\n') + '\n';
    fs.appendFileSync(outputFile, content, 'utf8');
  }
}

// Einstiegspunkt
function main() {
  const config = ConfigLoader.load();
  const args = ArgParser.parse();

  if (args.hilfe) {
    ArgParser.showHelp();
    return;
  }

  const log = new ProcessedLog(config.processed_log || 'processed.log');

  if (args.reset) {
    log.reset();
    return;
  }

  const cwd = ScriptInfo.cwd();
  const outputFile = path.resolve(cwd, config.output_file || 'output.md');
  const rootDirs = (args.scan || config.root_dirs || []).map(d => path.resolve(d));
  const ignoreDirs = (args.ignore || []).map(d => path.relative(cwd, path.resolve(d)).replace(/\\/g, '/'));
  const extensions = config.extensions || ['.md'];

  const newLinks: string[] = [];
  const newProcessed: string[] = [];

  for (const absPath of FileScanner.find(rootDirs, ignoreDirs, extensions)) {
    const relPath = path.relative(cwd, absPath).replace(/\\/g, '/');
    if (path.resolve(absPath) === outputFile || log.has(relPath)) continue;

    const title = TitleExtractor.extract(absPath);
    newLinks.push(`- [${title}](${relPath})`);
    newProcessed.push(relPath);
  }

  if (newLinks.length > 0) {
    MarkdownAppender.append(outputFile, newLinks);
    log.update(newProcessed);
    console.log(`✔ ${newLinks.length} neue Links hinzugefügt.`);
  } else {
    console.log(' ℹ  Keine neuen Dateien gefunden.');
  }
}

main();

