/**
 * Build-time data source for the segment catalog (`/segments`). Reads the committed
 * registry.json (produced by the Go guard in src/config/segment_catalog_test.go) plus the
 * docs/segments/*.mdx files, and exposes the merged, computed result via
 * usePluginData('oh-my-posh-segments'). See the "Data contract" section of the phase plan for
 * the exact shape this plugin must produce.
 */

const fs = require('fs');
const path = require('path');

const {
  AUTH_TIERS,
  AUTH_TIER_LABELS,
  GROUP_LABELS,
  DESCRIPTION_OVERRIDES,
} = require('./data');

const REGISTRY_PATH = path.join(__dirname, 'registry.json');

// Fixed display order for the "Setup" facet — not derived from the data, per the plan.
const AUTH_TIER_ORDER = ['oauth', 'cli', 'apikey', 'url'];

function readRegistry() {
  if (!fs.existsSync(REGISTRY_PATH)) {
    throw new Error(
      `oh-my-posh-segments plugin: missing ${REGISTRY_PATH}. This file is generated by the Go ` +
      'guard, not written by hand. Run `go test ./config/...` from the repo root to generate ' +
      'or refresh it, then re-run the website build.',
    );
  }

  let raw;
  try {
    raw = fs.readFileSync(REGISTRY_PATH, 'utf8');
  } catch (err) {
    throw new Error(`oh-my-posh-segments plugin: failed to read ${REGISTRY_PATH}: ${err.message}`);
  }

  let entries;
  try {
    entries = JSON.parse(raw);
  } catch (err) {
    throw new Error(`oh-my-posh-segments plugin: ${REGISTRY_PATH} is not valid JSON: ${err.message}`);
  }

  if (!Array.isArray(entries)) {
    throw new Error(`oh-my-posh-segments plugin: ${REGISTRY_PATH} must contain a JSON array.`);
  }

  const byDocId = new Map();
  for (const entry of entries) {
    if (!entry || typeof entry.docId !== 'string' || typeof entry.type !== 'string') {
      throw new Error(
        `oh-my-posh-segments plugin: malformed entry in ${REGISTRY_PATH}: ${JSON.stringify(entry)}. ` +
        'Expected { type, docId }.',
      );
    }
    if (byDocId.has(entry.docId)) {
      throw new Error(
        `oh-my-posh-segments plugin: duplicate docId "${entry.docId}" in ${REGISTRY_PATH}.`,
      );
    }
    byDocId.set(entry.docId, entry);
  }

  return byDocId;
}

// Parses the three flat `key: value` frontmatter lines shared by every doc in
// docs/segments/*/*.mdx. No YAML dependency is warranted for a format this fixed.
function parseFrontmatter(filePath, content) {
  const lines = content.split(/\r?\n/);

  if (lines[0] !== '---') {
    throw new Error(`oh-my-posh-segments plugin: ${filePath} does not start with a "---" frontmatter fence.`);
  }

  let end = -1;
  for (let i = 1; i < lines.length; i += 1) {
    if (lines[i] === '---') {
      end = i;
      break;
    }
  }
  if (end === -1) {
    throw new Error(`oh-my-posh-segments plugin: ${filePath} has no closing "---" frontmatter fence.`);
  }

  const fields = {};
  for (const line of lines.slice(1, end)) {
    const sep = line.indexOf(':');
    if (sep === -1) {
      continue;
    }
    const key = line.slice(0, sep).trim();
    const value = line.slice(sep + 1).trim();
    fields[key] = value;
  }

  if (!fields.title) {
    throw new Error(`oh-my-posh-segments plugin: ${filePath} frontmatter is missing "title".`);
  }

  return fields;
}

// Extracts the description: the first non-empty paragraph after the "## What" heading that is
// not part of a ::: admonition block. See website/docs/segments/system/battery.mdx, which opens
// with a :::caution immediately under "## What" — a naive "first paragraph" implementation
// would ship the WSL caution text as the battery description.
function extractDescription(filePath, content) {
  const lines = content.split(/\r?\n/);

  const whatIndex = lines.findIndex((line) => line.trim() === '## What');
  if (whatIndex === -1) {
    throw new Error(`oh-my-posh-segments plugin: ${filePath} has no "## What" section.`);
  }

  const section = [];
  for (const line of lines.slice(whatIndex + 1)) {
    if (line.startsWith('## ')) {
      break;
    }
    section.push(line);
  }

  const paragraphs = [];
  let current = [];
  let inAdmonition = false;

  for (const line of section) {
    if (line.startsWith(':::')) {
      if (!inAdmonition) {
        inAdmonition = true;
        if (current.length > 0) {
          paragraphs.push(current.join('\n'));
          current = [];
        }
      } else {
        inAdmonition = false;
      }
      continue;
    }

    if (inAdmonition) {
      continue;
    }

    if (line.trim() === '') {
      if (current.length > 0) {
        paragraphs.push(current.join('\n'));
        current = [];
      }
    } else {
      current.push(line);
    }
  }
  if (current.length > 0) {
    paragraphs.push(current.join('\n'));
  }

  if (paragraphs.length === 0) {
    throw new Error(
      `oh-my-posh-segments plugin: ${filePath} "## What" section has no paragraph outside of ` +
      'admonition blocks.',
    );
  }

  let description = paragraphs[0];

  // [text](url) and [text][ref] -> text
  description = description.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1');
  description = description.replace(/\[([^\]]+)\]\[[^\]]*\]/g, '$1');
  // [text] -> text, the shortcut reference form, where the definition at the
  // bottom of the file is keyed on the link text itself. Must run last, after
  // the two forms above have consumed their trailing brackets or parens.
  description = description.replace(/\[([^\]]+)\]/g, '$1');
  // `code` -> code
  description = description.replace(/`([^`]+)`/g, '$1');
  // collapse whitespace runs and join onto one line
  description = description.replace(/\s+/g, ' ').trim();

  if (description === '') {
    throw new Error(`oh-my-posh-segments plugin: ${filePath} "## What" paragraph is empty after normalisation.`);
  }
  if (description.startsWith(':::')) {
    throw new Error(
      `oh-my-posh-segments plugin: ${filePath} extracted description still starts with ":::" ` +
      '— the admonition skip failed for this file.',
    );
  }

  return description;
}

function capitalise(word) {
  return word.charAt(0).toUpperCase() + word.slice(1);
}

function groupLabelFor(group) {
  return GROUP_LABELS[group] || capitalise(group);
}

function loadSegments(docsRoot, registryByDocId) {
  const groupDirs = fs
    .readdirSync(docsRoot, { withFileTypes: true })
    .filter((entry) => entry.isDirectory())
    .map((entry) => entry.name)
    .sort();

  if (groupDirs.length === 0) {
    throw new Error(`oh-my-posh-segments plugin: no group directories found under ${docsRoot}.`);
  }

  const segments = [];
  const seenDocIds = new Set();

  for (const group of groupDirs) {
    const groupDir = path.join(docsRoot, group);
    const files = fs
      .readdirSync(groupDir, { withFileTypes: true })
      .filter((entry) => entry.isFile() && entry.name.endsWith('.mdx'))
      .map((entry) => entry.name)
      .filter((name) => name !== 'overview.mdx')
      .sort();

    for (const fileName of files) {
      const filePath = path.join(groupDir, fileName);
      const id = fileName.slice(0, -'.mdx'.length);

      if (seenDocIds.has(id)) {
        throw new Error(`oh-my-posh-segments plugin: duplicate doc id "${id}" (${filePath}).`);
      }
      seenDocIds.add(id);

      const content = fs.readFileSync(filePath, 'utf8');
      const frontmatter = parseFrontmatter(filePath, content);
      const description = extractDescription(filePath, content);

      const registryEntry = registryByDocId.get(id);
      if (!registryEntry) {
        throw new Error(
          `oh-my-posh-segments plugin: doc "${id}" (${filePath}) has no matching entry in ` +
          `${REGISTRY_PATH}. Regenerate the registry with \`go test ./config/...\`.`,
        );
      }

      // No groupLabel or href here: both are cheap to reconstruct client-side from group/id
      // (groupLabel via the label already carried on this segment's entry in the emitted
      // groups[] array, href as `/docs/segments/${group}/${id}`) and shipping them anyway would
      // be ~117 duplicate copies of a value derivable from two fields already present, riding in
      // the shared main.js chunk on every page.
      segments.push({
        id,
        type: registryEntry.type,
        name: frontmatter.title,
        group,
        description: Object.prototype.hasOwnProperty.call(DESCRIPTION_OVERRIDES, id)
          ? DESCRIPTION_OVERRIDES[id]
          : description,
        auth: AUTH_TIERS[id] || null,
      });
    }
  }

  // registry.json must not contain entries with no corresponding doc.
  const missingDocs = [];
  for (const docId of registryByDocId.keys()) {
    if (!seenDocIds.has(docId)) {
      missingDocs.push(docId);
    }
  }
  if (missingDocs.length > 0) {
    throw new Error(
      `oh-my-posh-segments plugin: ${REGISTRY_PATH} has ${missingDocs.length} entr${missingDocs.length === 1 ? 'y' : 'ies'} ` +
      `with no matching doc file: ${missingDocs.sort().join(', ')}.`,
    );
  }

  // Every curated key must reference a real doc id, so a rename or removal fails loudly here
  // instead of silently dropping out of the catalog.
  const unknownAuthKeys = Object.keys(AUTH_TIERS).filter((id) => !seenDocIds.has(id));
  if (unknownAuthKeys.length > 0) {
    throw new Error(
      `oh-my-posh-segments plugin: AUTH_TIERS in data.js references unknown doc id(s): ${unknownAuthKeys.join(', ')}.`,
    );
  }

  const unknownOverrideKeys = Object.keys(DESCRIPTION_OVERRIDES).filter((id) => !seenDocIds.has(id));
  if (unknownOverrideKeys.length > 0) {
    throw new Error(
      `oh-my-posh-segments plugin: DESCRIPTION_OVERRIDES in data.js references unknown doc id(s): ${unknownOverrideKeys.join(', ')}.`,
    );
  }

  const unknownAuthValues = Object.entries(AUTH_TIERS).filter(([, tier]) => !AUTH_TIER_LABELS[tier]);
  if (unknownAuthValues.length > 0) {
    throw new Error(
      `oh-my-posh-segments plugin: AUTH_TIERS in data.js uses tier(s) with no label in AUTH_TIER_LABELS: ` +
      `${unknownAuthValues.map(([id, tier]) => `${id} -> ${tier}`).join(', ')}.`,
    );
  }

  segments.sort((a, b) => {
    if (a.group !== b.group) {
      return a.group < b.group ? -1 : 1;
    }
    return a.name.localeCompare(b.name);
  });

  return segments;
}

// count is used here to sort groups by popularity (biggest facet first) but is dropped before
// the result is emitted below - SegmentCatalog recomputes live, filter-aware counts itself
// (see its groupCount) rather than reading this build-time snapshot, so shipping it too would
// just be another 117-ish duplicate riding in the shared chunk for no consumer.
function computeGroups(segments) {
  const counts = new Map();
  for (const segment of segments) {
    counts.set(segment.group, (counts.get(segment.group) || 0) + 1);
  }

  return Array.from(counts.entries())
    .map(([id, count]) => ({ id, label: groupLabelFor(id), count }))
    .sort((a, b) => b.count - a.count)
    .map(({ id, label }) => ({ id, label }));
}

// Same story as computeGroups: count drives AUTH_TIER_ORDER's filter (only emit tiers that are
// actually used) but SegmentCatalog computes its own live authCount, so it is dropped here too.
function computeAuthTiers(segments) {
  const counts = new Map();
  for (const segment of segments) {
    if (segment.auth) {
      counts.set(segment.auth, (counts.get(segment.auth) || 0) + 1);
    }
  }

  return AUTH_TIER_ORDER
    .filter((id) => counts.has(id))
    .map((id) => ({ id, label: AUTH_TIER_LABELS[id] }));
}

// Static assets (website/static/llms.txt, llms-full.txt, skill.md, skills/ohmyposh/SKILL.md) cite
// the segment count as plain prose because they are served as-is and can't call usePluginData like
// everything else on the site does. That means nothing catches them drifting from the real count
// as segments are added or removed - this is that something.
const STATIC_ASSETS_CITING_TOTAL = [
  path.join(__dirname, '../../static/llms.txt'),
  path.join(__dirname, '../../static/llms-full.txt'),
  path.join(__dirname, '../../static/skill.md'),
  path.join(__dirname, '../../static/skills/ohmyposh/SKILL.md'),
];

function assertStaticAssetsMatchTotal(total) {
  const mentionsTotal = new RegExp(`(?<!\\d)${total}(?!\\d)`);

  for (const filePath of STATIC_ASSETS_CITING_TOTAL) {
    const content = fs.readFileSync(filePath, 'utf8');

    if (!mentionsTotal.test(content)) {
      throw new Error(
        `oh-my-posh-segments plugin: ${filePath} no longer mentions "${total}" segments. It is a ` +
        "static asset that can't read plugin data, so its segment count is a hand-maintained " +
        `copy of the real one - update it to ${total} to match.`,
      );
    }
  }
}

module.exports = function segmentsPlugin(context) {
  return {
    name: 'oh-my-posh-segments',

    async loadContent() {
      const registryByDocId = readRegistry();
      const docsRoot = path.join(context.siteDir, 'docs', 'segments');

      const segments = loadSegments(docsRoot, registryByDocId);
      const groups = computeGroups(segments);
      const authTiers = computeAuthTiers(segments);
      const total = segments.length;

      assertStaticAssetsMatchTotal(total);

      return {
        segments,
        groups,
        authTiers,
        total,
      };
    },

    async contentLoaded({ content, actions }) {
      actions.setGlobalData(content);
    },
  };
};
