// ABOUTME: Parser for SKILL.md files with YAML frontmatter.
// ABOUTME: Extracts name and description per the Agent Skills spec.

import type { SkillMetadata } from "./types";

/**
 * Result of parsing a SKILL.md file.
 */
export interface ParsedSkill {
  metadata: SkillMetadata;
  content: string;
  rawContent: string;
}

const SKILL_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;

/**
 * Parse YAML frontmatter from a SKILL.md file.
 * Frontmatter is delimited by --- at the start of the file.
 */
export function parseSkillMd(rawContent: string): ParsedSkill {
  const trimmed = rawContent.trim();

  // Check for frontmatter delimiter
  if (!trimmed.startsWith("---")) {
    // No frontmatter, treat entire content as the skill description
    return {
      metadata: extractMetadataFromContent(trimmed),
      content: trimmed,
      rawContent,
    };
  }

  // Find the closing delimiter
  const endIndex = trimmed.indexOf("---", 3);
  if (endIndex === -1) {
    // No closing delimiter, treat as no frontmatter
    return {
      metadata: extractMetadataFromContent(trimmed),
      content: trimmed,
      rawContent,
    };
  }

  const frontmatter = trimmed.slice(3, endIndex).trim();
  const content = trimmed.slice(endIndex + 3).trim();

  const metadata = parseYamlFrontmatter(frontmatter);

  // If no name in frontmatter, try to extract from content heading
  if (!metadata.name) {
    const nameFromContent = extractSkillHeading(content);
    if (nameFromContent) {
      metadata.name = nameFromContent;
    }
  }

  return {
    metadata,
    content,
    rawContent,
  };
}

/**
 * Parse YAML-like frontmatter into metadata.
 * Extracts only spec-required fields: name and description.
 * Other fields (version, author, tags) come from the skills catalog API.
 */
function parseYamlFrontmatter(yaml: string): SkillMetadata {
  const metadata: SkillMetadata = {
    name: "",
    description: "",
  };

  const lines = yaml.split("\n");
  let currentKey: string | null = null;
  let inArray = false;

  for (const line of lines) {
    const trimmedLine = line.trim();

    // Skip empty lines, comments, and indented lines (metadata sub-keys)
    if (!trimmedLine || trimmedLine.startsWith("#") || line.startsWith(" ")) {
      continue;
    }

    // Check for array item
    if (trimmedLine.startsWith("- ") && currentKey && inArray) {
      const value = trimmedLine
        .slice(2)
        .trim()
        .replace(/^["']|["']$/g, "");
      if (currentKey === "tags") {
        metadata.tags = [...(metadata.tags ?? []), value];
      }
      if (currentKey === "requires") {
        metadata.requires = [...(metadata.requires ?? []), value];
      }
      if (currentKey === "globs") {
        metadata.globs = [...(metadata.globs ?? []), value];
      }
      if (currentKey === "alwaysAllow") {
        metadata.alwaysAllow = [...(metadata.alwaysAllow ?? []), value];
      }
      if (currentKey === "includes") {
        metadata.includes = [...(metadata.includes ?? []), value];
      }
      if (currentKey === "exclude-hosts" || currentKey === "excludeHosts") {
        metadata.excludeHosts = [...(metadata.excludeHosts ?? []), value];
      }
      continue;
    }

    // Check for key-value pair
    const colonIndex = trimmedLine.indexOf(":");
    if (colonIndex > 0) {
      const key = trimmedLine.slice(0, colonIndex).trim();
      const value = trimmedLine.slice(colonIndex + 1).trim();
      currentKey = key;

      // Check if this is the start of an array (empty value or explicit array)
      if (!value || value === "[]") {
        inArray = true;
        if (key === "tags") metadata.tags = [];
        if (key === "requires") metadata.requires = [];
        if (key === "globs") metadata.globs = [];
        if (key === "alwaysAllow") metadata.alwaysAllow = [];
        if (key === "includes") metadata.includes = [];
        if (key === "exclude-hosts" || key === "excludeHosts")
          metadata.excludeHosts = [];
        continue;
      }

      inArray = false;

      // Handle inline arrays [item1, item2]
      if (value.startsWith("[") && value.endsWith("]")) {
        const items = value
          .slice(1, -1)
          .split(",")
          .map((s) => s.trim().replace(/^["']|["']$/g, ""))
          .filter(Boolean);

        if (key === "tags") metadata.tags = items;
        if (key === "requires") metadata.requires = items;
        if (key === "globs") metadata.globs = items;
        if (key === "alwaysAllow") metadata.alwaysAllow = items;
        if (key === "includes") metadata.includes = items;
        if (key === "exclude-hosts" || key === "excludeHosts")
          metadata.excludeHosts = items;
        continue;
      }

      // Handle scalar values
      const cleanValue = value.replace(/^["']|["']$/g, "");

      switch (key) {
        case "name":
          metadata.name = cleanValue;
          break;
        case "slug":
          metadata.slug = cleanValue;
          break;
        case "display-name":
          metadata.displayName = cleanValue;
          break;
        case "description":
          metadata.description = cleanValue;
          break;
        case "version":
          metadata.version = cleanValue;
          break;
        case "author":
          metadata.author = cleanValue;
          break;
      }
    }
  }

  return metadata;
}

/**
 * Extract metadata from content when no frontmatter is present.
 * Uses the first heading as name and first paragraph as description.
 */
function extractMetadataFromContent(content: string): SkillMetadata {
  const name = extractSkillHeading(content) || "Unnamed Skill";
  const description = extractDescriptionFromContent(content) || "";

  return {
    name,
    description,
  };
}

/**
 * Extract the skill name from the first markdown heading.
 */
export function extractSkillHeading(content: string): string | null {
  const headingMatch = content.match(/^#\s+(.+)$/m);
  return headingMatch ? headingMatch[1].trim() : null;
}

function humanizeSlug(value: string): string {
  return value
    .replace(/[-_]+/g, " ")
    .replace(/\b\w/g, (char) => char.toUpperCase());
}

/**
 * Humanize a skill name from a catalog index entry.
 * Returns the name as-is if it already has spaces or capitalization,
 * humanizes it if it looks like a slug, and falls back to the slug
 * if the name is empty.
 *
 * Used by the catalog ingestion path where the index `name` field
 * is often the directory basename (e.g. "backtester", "grid-trader").
 */
export function humanizeSkillName(name: string, fallbackSlug?: string): string {
  const trimmed = name?.trim() ?? "";
  if (trimmed) {
    return SKILL_SLUG_PATTERN.test(trimmed) ? humanizeSlug(trimmed) : trimmed;
  }
  if (fallbackSlug) {
    return humanizeSlug(fallbackSlug);
  }
  return "Unnamed Skill";
}

/**
 * Resolve a human-friendly skill name.
 * Prefers Markdown H1, then metadata name, then fallback slug.
 */
export function resolveSkillDisplayName(
  parsed: ParsedSkill,
  fallbackSlug?: string,
): string {
  const heading = extractSkillHeading(parsed.content);
  if (heading) {
    return heading;
  }

  const metadataName = parsed.metadata.name?.trim();
  if (metadataName) {
    return SKILL_SLUG_PATTERN.test(metadataName)
      ? humanizeSlug(metadataName)
      : metadataName;
  }

  if (fallbackSlug) {
    return humanizeSlug(fallbackSlug);
  }

  return "Unnamed Skill";
}

/**
 * Derive a URL-friendly slug from a display name.
 * "Polymarket Bot" → "polymarket-bot"
 * Returns null if the name can't produce a valid slug.
 */
export function slugFromName(name: string): string | null {
  const slug = name
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-|-$/g, "");
  return SKILL_SLUG_PATTERN.test(slug) ? slug : null;
}

/**
 * Best-effort normalization for user-entered skill names. Always returns a
 * non-empty slug; falls back to `"skill"` if normalization would otherwise
 * produce an empty string. Intended for UI flows like the Create dialog
 * where we always want to scaffold something rather than fail validation.
 */
export function normalizeSkillSlug(raw: string): string {
  const normalized = raw
    .toLowerCase()
    .replace(/[^a-z0-9-]+/g, "-")
    .replace(/-+/g, "-")
    .replace(/^-|-$/g, "");
  return normalized || "skill";
}

/**
 * Resolve the canonical slug for an installed skill.
 * Priority: explicit slug field > name-derived slug > directory name.
 */
export function resolveSkillSlug(parsed: ParsedSkill, dirName: string): string {
  if (parsed.metadata.slug && SKILL_SLUG_PATTERN.test(parsed.metadata.slug)) {
    return parsed.metadata.slug;
  }

  const nameSlug = parsed.metadata.name
    ? slugFromName(parsed.metadata.name)
    : null;
  if (nameSlug && nameSlug !== dirName) {
    return nameSlug;
  }

  return dirName;
}

/**
 * Extract description from the first non-heading paragraph.
 */
function extractDescriptionFromContent(content: string): string | null {
  const lines = content.split("\n");
  let foundHeading = false;

  for (const line of lines) {
    const trimmed = line.trim();

    // Skip empty lines
    if (!trimmed) continue;

    // Skip headings
    if (trimmed.startsWith("#")) {
      foundHeading = true;
      continue;
    }

    // Return first non-heading, non-empty line after a heading
    if (foundHeading && trimmed) {
      return trimmed;
    }
  }

  return null;
}

/**
 * Compute SHA-256 hash of content for change detection.
 */
export async function computeContentHash(content: string): Promise<string> {
  return computeBytesHash(new TextEncoder().encode(content));
}

/**
 * Compute SHA-256 hash of raw bytes. Binary-safe counterpart of
 * computeContentHash; for valid UTF-8 text the two agree (#2297).
 */
export async function computeBytesHash(bytes: Uint8Array): Promise<string> {
  const hashBuffer = await crypto.subtle.digest(
    "SHA-256",
    bytes as BufferSource,
  );
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
}
