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

/**
 * SKILL.md / api-reference.md parser
 *
 * Extracts structured data from markdown technique files:
 * - YAML frontmatter (name, description, tags, mitre-attack, tools)
 * - Tables (as arrays of row objects keyed by header)
 * - Code blocks (with language tag)
 * - Sections (h2/h3 hierarchy)
 * - ATT&CK technique IDs (TXxxx.xxx pattern)
 */

/**
 * Parse a SKILL.md or api-reference.md file into structured data.
 * @param {string} raw - raw markdown content
 * @returns {ParsedSkill}
 */
export function parseSkillMd(raw) {
  const { frontmatter, body } = extractFrontmatter(raw);
  const sections = extractSections(body);
  const tables = extractTables(body);
  const codeBlocks = extractCodeBlocks(body);
  const attackIds = extractAttackIds(raw);
  const tools = frontmatter?.metadata?.tools || extractToolNames(raw);

  return {
    frontmatter,
    sections,
    tables,
    codeBlocks,
    attackIds,
    tools,
    raw: body,
  };
}

/**
 * Extract YAML frontmatter between --- delimiters.
 * Lightweight parser — handles scalar, list, and nested map values.
 */
function extractFrontmatter(raw) {
  // Strip leading HTML comments (copyright headers) before looking for frontmatter
  const stripped = raw.replace(/^(?:\s*<!--[\s\S]*?-->\s*)+/, '');
  const match = stripped.match(/^---\r?\n([\s\S]*?)\r?\n---/);
  if (!match) return { frontmatter: {}, body: raw };

  const yaml = match[1];
  // Calculate body from stripped string (comments already removed)
  const body = stripped.slice(match[0].length).trim();
  const frontmatter = parseSimpleYaml(yaml);
  return { frontmatter, body };
}

/**
 * Minimal YAML parser — supports scalars, arrays (- item), and one level of nesting.
 */
function parseSimpleYaml(yaml) {
  const result = {};
  let currentKey = null;
  let currentList = null;
  let nestedObj = null;
  let nestedKey = null;

  for (const line of yaml.split('\n')) {
    // Blank line
    if (!line.trim()) continue;

    // List item (2-space indent)
    const listMatch = line.match(/^ {2}- (.+)$/);
    if (listMatch && currentKey) {
      if (!currentList) currentList = [];
      let val = listMatch[1].trim();
      // Strip quotes
      if ((val.startsWith('"') && val.endsWith('"')) ||
          (val.startsWith("'") && val.endsWith("'"))) {
        val = val.slice(1, -1);
      }
      currentList.push(val);
      continue;
    }

    // Nested key (2-space indent, key: value)
    const nestedMatch = line.match(/^ {2}(\w[\w-]*)\s*:\s*(.+)?$/);
    if (nestedMatch && nestedKey) {
      if (!nestedObj) nestedObj = {};
      const nk = nestedMatch[1];
      let nv = nestedMatch[2]?.trim() || '';
      // Handle inline list: ["T1222.001", "T1484.001"]
      if (nv.startsWith('[') && nv.endsWith(']')) {
        nv = nv.slice(1, -1).split(',').map(s => {
          s = s.trim();
          return (s.startsWith('"') || s.startsWith("'")) ? s.slice(1, -1) : s;
        });
      }
      nestedObj[nk] = nv;
      continue;
    }

    // Flush pending list/nested
    if (currentList && currentKey) {
      result[currentKey] = currentList;
      currentList = null;
    }
    if (nestedObj && nestedKey) {
      result[nestedKey] = nestedObj;
      nestedObj = null;
      nestedKey = null;
    }

    // Top-level key: value
    const kvMatch = line.match(/^(\w[\w-]*)\s*:\s*(.*)?$/);
    if (kvMatch) {
      currentKey = kvMatch[1];
      let val = kvMatch[2]?.trim() || '';

      if (val === '' || val === '>-') {
        // Possibly a list, nested object, or multi-line scalar follows
        if (val === '>-') {
          // Folded scalar — collect following indented lines
          currentKey = kvMatch[1];
          // We'll handle this by checking next lines
          result[currentKey] = '';
          continue;
        }
        // Could be list or nested — peek at next lines
        nestedKey = currentKey;
        continue;
      }

      // Strip quotes
      if ((val.startsWith('"') && val.endsWith('"')) ||
          (val.startsWith("'") && val.endsWith("'"))) {
        val = val.slice(1, -1);
      }
      // Inline list
      if (val.startsWith('[') && val.endsWith(']')) {
        val = val.slice(1, -1).split(',').map(s => {
          s = s.trim();
          return (s.startsWith('"') || s.startsWith("'")) ? s.slice(1, -1) : s;
        });
      }
      result[currentKey] = val;
      currentList = null;
      nestedKey = null;
    } else {
      // Continuation of folded scalar
      const trimmed = line.trim();
      if (currentKey && typeof result[currentKey] === 'string') {
        result[currentKey] = result[currentKey]
          ? result[currentKey] + ' ' + trimmed
          : trimmed;
      }
    }
  }

  // Flush trailing
  if (currentList && currentKey) result[currentKey] = currentList;
  if (nestedObj && nestedKey) result[nestedKey] = nestedObj;

  return result;
}

/**
 * Extract markdown sections (h2 and h3).
 * Returns array of { level, title, content }.
 */
function extractSections(body) {
  const sections = [];
  const lines = body.split('\n');
  let current = null;
  const contentLines = [];

  for (const line of lines) {
    const h2 = line.match(/^## (.+)$/);
    const h3 = line.match(/^### (.+)$/);

    if (h2 || h3) {
      if (current) {
        current.content = contentLines.splice(0).join('\n').trim();
        sections.push(current);
      }
      current = {
        level: h2 ? 2 : 3,
        title: (h2 || h3)[1].trim(),
        content: '',
      };
    } else {
      contentLines.push(line);
    }
  }

  if (current) {
    current.content = contentLines.join('\n').trim();
    sections.push(current);
  }

  return sections;
}

/**
 * Extract markdown tables.
 * Returns array of { headers: string[], rows: object[] }.
 */
function extractTables(body) {
  const tables = [];
  const lines = body.split('\n');
  let i = 0;

  while (i < lines.length) {
    const line = lines[i];
    // Detect table header row: | Header | Header |
    if (line.match(/^\|.+\|$/) && i + 1 < lines.length && lines[i + 1].match(/^\|[-:\s|]+\|$/)) {
      const headers = line.split('|').slice(1, -1).map(h => h.trim());
      i += 2; // skip separator row
      const rows = [];
      while (i < lines.length && lines[i].match(/^\|.+\|$/)) {
        const cells = lines[i].split('|').slice(1, -1).map(c => c.trim());
        const row = {};
        headers.forEach((h, idx) => { row[h] = cells[idx] || ''; });
        rows.push(row);
        i++;
      }
      tables.push({ headers, rows });
    } else {
      i++;
    }
  }

  return tables;
}

/**
 * Extract fenced code blocks.
 * Returns array of { lang, code, context }.
 * context = the heading above the code block (nearest h2/h3).
 */
function extractCodeBlocks(body) {
  const blocks = [];
  const lines = body.split('\n');
  let currentHeading = '';

  let i = 0;
  while (i < lines.length) {
    // Track headings
    const hMatch = lines[i].match(/^#{2,3} (.+)$/);
    if (hMatch) {
      currentHeading = hMatch[1].trim();
      i++;
      continue;
    }

    // Detect code fence
    const fenceMatch = lines[i].match(/^```(\w*)$/);
    if (fenceMatch) {
      const lang = fenceMatch[1] || 'text';
      i++;
      const codeLines = [];
      while (i < lines.length && !lines[i].match(/^```$/)) {
        codeLines.push(lines[i]);
        i++;
      }
      blocks.push({
        lang,
        code: codeLines.join('\n'),
        context: currentHeading,
      });
      i++; // skip closing fence
    } else {
      i++;
    }
  }

  return blocks;
}

/**
 * Extract MITRE ATT&CK technique IDs from text.
 * Matches T####, T####.###
 */
function extractAttackIds(text) {
  const ids = new Set();
  const re = /T\d{4}(?:\.\d{3})?/g;
  let m;
  while ((m = re.exec(text))) {
    ids.add(m[0]);
  }
  return [...ids];
}

/**
 * Extract tool names from markdown.
 * Looks for tool mentions in headings, table cells, and code blocks.
 */
function extractToolNames(raw) {
  // Known tool patterns
  const known = [
    'nmap', 'nikto', 'burpsuite', 'metasploit', 'wireshark', 'bloodhound',
    'powerview', 'impacket', 'certipy', 'nuclei', 'katana', 'ffuf', 'gobuster',
    'hashcat', 'john', 'hydra', 'sqlmap', 'subfinder', 'amass', 'shodan',
    'ghidra', 'ida', 'gdb', 'radare2', 'frida', 'mimikatz', 'rubeus',
    'crackmapexec', 'covenant', 'sliver', 'cobalt strike', 'empire',
    'volatility', 'autopsy', 'velociraptor', 'osquery', 'sigma', 'yara',
    'snort', 'suricata', 'zeek', 'sysmon', 'wevtutil', 'auditd',
    'terraform', 'ansible', 'prowler', 'scoutsuite', 'checkov',
    'trivy', 'grype', 'syft', 'cosign', 'falco', 'caldera', 'atomic red team',
    'bloodyad', 'certipy', 'petitpotam', 'responder', 'bettercap',
  ];
  const found = [];
  const lower = raw.toLowerCase();
  for (const tool of known) {
    if (lower.includes(tool)) found.push(tool);
  }
  return [...new Set(found)];
}
