// Copyright (c) 2026 defconxt. All rights reserved.
// Licensed under AGPL-3.0 — see LICENSE file for details.
// NERF is a trademark of defconxt.

/**
 * NERF Semantic Document Sharding
 *
 * Splits large markdown documents into semantically coherent chunks
 * at heading boundaries. Produces numbered shard files with an index.
 *
 * @module analyze/sharding
 */

import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
import { join, basename, dirname, extname } from 'node:path';

// ---------------------------------------------------------------------------
// Shard
// ---------------------------------------------------------------------------

/**
 * A single document shard.
 */
export class Shard {
  /**
   * @param {object} opts
   * @param {number} opts.number   - Shard number (1-indexed)
   * @param {string} opts.title    - Section heading
   * @param {string} opts.content  - Section content (including heading)
   * @param {number} opts.lines    - Line count
   * @param {number} opts.startLine - Start line in source (1-indexed)
   * @param {number} opts.endLine   - End line in source (1-indexed)
   */
  constructor(opts = {}) {
    this.number = opts.number ?? 0;
    this.title = opts.title ?? '';
    this.content = opts.content ?? '';
    this.lines = opts.lines ?? 0;
    this.startLine = opts.startLine ?? 0;
    this.endLine = opts.endLine ?? 0;
  }

  /** Generate shard filename. */
  filename(sourceBase) {
    const padded = String(this.number).padStart(2, '0');
    const slug = this.title
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, '-')
      .replace(/^-|-$/g, '')
      .slice(0, 50);
    return `${sourceBase}-${padded}-${slug}.md`;
  }

  /** Generate shard content with frontmatter. */
  toFile(sourcePath, totalShards) {
    return [
      '---',
      `source: ${sourcePath}`,
      `shard: ${this.number}/${totalShards}`,
      `title: "${this.title}"`,
      `lines: ${this.startLine}-${this.endLine}`,
      '---',
      '',
      this.content,
    ].join('\n');
  }
}

// ---------------------------------------------------------------------------
// ShardResult
// ---------------------------------------------------------------------------

export class ShardResult {
  /**
   * @param {object} opts
   * @param {Shard[]} opts.shards
   * @param {string} opts.sourcePath
   * @param {number} opts.sourceLines
   * @param {number} opts.headingLevel
   */
  constructor(opts = {}) {
    this.shards = opts.shards ?? [];
    this.sourcePath = opts.sourcePath ?? '';
    this.sourceLines = opts.sourceLines ?? 0;
    this.headingLevel = opts.headingLevel ?? 2;
  }

  /** Generate index content. */
  toIndex() {
    const lines = [
      `# Shard Index — ${basename(this.sourcePath)}`,
      '',
      `Source: \`${this.sourcePath}\``,
      `Total lines: ${this.sourceLines}`,
      `Heading level: H${this.headingLevel}`,
      `Shards: ${this.shards.length}`,
      '',
      '| # | Title | Lines | Range |',
      '|---|-------|-------|-------|',
    ];
    const sourceBase = basename(this.sourcePath, extname(this.sourcePath));
    for (const shard of this.shards) {
      const fn = shard.filename(sourceBase);
      lines.push(`| ${shard.number} | [${shard.title}](${fn}) | ${shard.lines} | ${shard.startLine}-${shard.endLine} |`);
    }
    return lines.join('\n');
  }

  toReport() {
    const lines = [
      `Sharding: ${basename(this.sourcePath)}`,
      `  Source: ${this.sourceLines} lines → ${this.shards.length} shards at H${this.headingLevel}`,
      '',
    ];
    for (const shard of this.shards) {
      lines.push(`  [${String(shard.number).padStart(2, '0')}] ${shard.title} (${shard.lines} lines)`);
    }
    return lines.join('\n');
  }

  toJSON() {
    return {
      sourcePath: this.sourcePath,
      sourceLines: this.sourceLines,
      headingLevel: this.headingLevel,
      shardCount: this.shards.length,
      shards: this.shards.map((s) => ({
        number: s.number,
        title: s.title,
        lines: s.lines,
        startLine: s.startLine,
        endLine: s.endLine,
      })),
    };
  }
}

// ---------------------------------------------------------------------------
// Sharding logic
// ---------------------------------------------------------------------------

/**
 * Split a markdown document into shards at heading boundaries.
 *
 * @param {string} filePath     - Path to the markdown file
 * @param {object} [options]
 * @param {number} [options.level=2]     - Heading level to split at (1-6)
 * @param {string} [options.output]      - Output directory (default: <filename>-shards/)
 * @param {boolean} [options.dryRun=false] - Preview only, don't write files
 * @returns {ShardResult}
 */
export function shardDocument(filePath, options = {}) {
  const level = options.level ?? 2;
  const dryRun = options.dryRun ?? false;

  if (!existsSync(filePath)) {
    throw new Error(`File not found: ${filePath}`);
  }

  const content = readFileSync(filePath, 'utf-8');
  const lines = content.split('\n');
  const headingPrefix = '#'.repeat(level) + ' ';

  // Find heading positions
  const headings = [];
  for (let i = 0; i < lines.length; i++) {
    if (lines[i].startsWith(headingPrefix)) {
      headings.push({
        line: i,
        title: lines[i].slice(headingPrefix.length).trim(),
      });
    }
  }

  // Handle edge cases
  if (headings.length === 0) {
    // No headings at this level — return entire doc as one shard
    return new ShardResult({
      shards: [new Shard({
        number: 1,
        title: basename(filePath, extname(filePath)),
        content: content,
        lines: lines.length,
        startLine: 1,
        endLine: lines.length,
      })],
      sourcePath: filePath,
      sourceLines: lines.length,
      headingLevel: level,
    });
  }

  // Build shards
  const shards = [];

  // Preamble (content before first heading)
  if (headings[0].line > 0) {
    const preambleContent = lines.slice(0, headings[0].line).join('\n').trim();
    if (preambleContent.length > 0) {
      shards.push(new Shard({
        number: shards.length + 1,
        title: 'Preamble',
        content: preambleContent,
        lines: headings[0].line,
        startLine: 1,
        endLine: headings[0].line,
      }));
    }
  }

  // Heading sections
  for (let h = 0; h < headings.length; h++) {
    const start = headings[h].line;
    const end = h + 1 < headings.length ? headings[h + 1].line : lines.length;
    const sectionContent = lines.slice(start, end).join('\n').trimEnd();

    shards.push(new Shard({
      number: shards.length + 1,
      title: headings[h].title,
      content: sectionContent,
      lines: end - start,
      startLine: start + 1,
      endLine: end,
    }));
  }

  const result = new ShardResult({
    shards,
    sourcePath: filePath,
    sourceLines: lines.length,
    headingLevel: level,
  });

  // Write files unless dry run
  if (!dryRun) {
    const sourceBase = basename(filePath, extname(filePath));
    const outputDir = options.output ?? join(dirname(filePath), `${sourceBase}-shards`);
    mkdirSync(outputDir, { recursive: true });

    // Write index
    writeFileSync(join(outputDir, 'INDEX.md'), result.toIndex());

    // Write shard files
    for (const shard of shards) {
      const fn = shard.filename(sourceBase);
      writeFileSync(join(outputDir, fn), shard.toFile(filePath, shards.length));
    }
  }

  return result;
}
