// file: libs/sdk/src/skill/skill.utils.ts

import { depsOfClass, getMetadata, isClass, type Token, type Type } from '@frontmcp/di';
import { fileExists, isAbsolute, pathJoin, readdir, readFile, stat } from '@frontmcp/utils';

import {
  extendedSkillMetadata,
  FrontMcpSkillTokens,
  isFileInstructions,
  isInlineInstructions,
  isUrlInstructions,
  normalizeToolRef,
  skillCallerDir,
  SkillKind,
  type SkillContext,
  type SkillInstructionSource,
  type SkillMetadata,
  type SkillRecord,
  type SkillType,
} from '../common';
import { type SkillContent, type SkillExampleInfo, type SkillReferenceInfo } from '../common/interfaces';
import { InvalidInstructionSourceError, InvalidSkillError, SkillInstructionFetchError } from '../errors';
import { stripFrontmatter } from './skill-md-parser';

/**
 * Collect skill metadata from a decorated class.
 *
 * @param cls - The class to collect metadata from
 * @returns The skill metadata
 */
export function collectSkillMetadata(cls: SkillType): SkillMetadata {
  const extended = getMetadata(extendedSkillMetadata, cls);
  const seed = (extended ? { ...extended } : {}) as SkillMetadata;

  return Object.entries(FrontMcpSkillTokens).reduce((metadata, [key, token]) => {
    const value = getMetadata(token, cls);
    if (value !== undefined) {
      return Object.assign(metadata, { [key]: value });
    }
    return metadata;
  }, seed);
}

/**
 * Normalize any skill input to a SkillRecord.
 *
 * Handles:
 * - Class with @Skill decorator → SkillClassTokenRecord
 * - SkillValueRecord (from skill() helper) → passthrough
 * - SkillFileRecord → passthrough
 *
 * @param item - The skill input to normalize
 * @returns A normalized SkillRecord
 * @throws Error if the input is invalid
 */
export function normalizeSkill(item: unknown): SkillRecord {
  // Check if it's already a SkillRecord (from skill() helper or file loader)
  if (isSkillRecord(item)) {
    return item;
  }

  // Check if it's a class with @Skill decorator
  if (isClass(item)) {
    const metadata = collectSkillMetadata(item as SkillType);
    if (!metadata.name) {
      const className = (item as object).constructor?.name ?? String(item);
      throw new InvalidSkillError(className, 'Class must be decorated with @Skill decorator and have a name property.');
    }
    const callerDir = getMetadata(skillCallerDir, item as SkillType) as string | undefined;
    return {
      kind: SkillKind.CLASS_TOKEN,
      provide: item as Type<SkillContext>,
      metadata,
      ...(callerDir ? { callerDir } : {}),
    };
  }

  // Invalid input
  const name = typeof item === 'object' && item !== null ? (item as Record<string, unknown>)['name'] : String(item);
  throw new InvalidSkillError(
    String(name ?? 'unknown'),
    'Expected a class decorated with @Skill or a skill() helper result.',
  );
}

/**
 * Check if an object is a SkillRecord.
 */
export function isSkillRecord(item: unknown): item is SkillRecord {
  if (typeof item !== 'object' || item === null) {
    return false;
  }

  const record = item as Record<string, unknown>;

  // Check for kind property
  if (!record['kind'] || typeof record['kind'] !== 'string') {
    return false;
  }

  // Validate kind is one of the allowed values
  const validKinds = [SkillKind.CLASS_TOKEN, SkillKind.VALUE, SkillKind.FILE, SkillKind.ESM, SkillKind.REMOTE];
  if (!validKinds.includes(record['kind'] as SkillKind)) {
    return false;
  }

  // Check for required properties
  if (!record['provide'] || !record['metadata']) {
    return false;
  }

  return true;
}

/**
 * Get dependency tokens from a skill record for DI graph validation.
 *
 * @param rec - The skill record
 * @returns Array of dependency tokens
 */
export function skillDiscoveryDeps(rec: SkillRecord): Token[] {
  switch (rec.kind) {
    case SkillKind.CLASS_TOKEN:
      return depsOfClass(rec.provide, 'discovery');
    case SkillKind.VALUE:
    case SkillKind.FILE:
    case SkillKind.ESM:
    case SkillKind.REMOTE:
      // Value, file, ESM, and remote records don't have class dependencies
      return [];
  }
}

/**
 * Load instructions from a skill instruction source.
 *
 * @param source - The instruction source (inline, file, or URL)
 * @param basePath - Base path for resolving relative file paths
 * @returns The loaded instructions as a string
 */
export async function loadInstructions(source: SkillInstructionSource, basePath?: string): Promise<string> {
  if (isInlineInstructions(source)) {
    return source;
  }

  if (isFileInstructions(source)) {
    // Resolve file path. When `source.file` is already absolute (callers like
    // `resolveFromSkillManifest` pass absolute paths), prepending `basePath`
    // produces nonsense like `/abs/bundle/dir//abs/user/file`. Guard with
    // `isAbsolute` so absolute paths flow through untouched (fix for the
    // PR #419 CI regression — `demo-e2e-skills` SKILL.md failed with
    // `libs/sdk/dist//home/runner/.../SKILL.md`).
    const filePath = isAbsolute(source.file) ? source.file : basePath ? pathJoin(basePath, source.file) : source.file;
    const content = await readFile(filePath, 'utf-8');
    // Strip YAML frontmatter if present (e.g., SKILL.md files)
    return stripFrontmatter(content);
  }

  if (isUrlInstructions(source)) {
    // Fetch from URL
    const response = await fetch(source.url);
    if (!response.ok) {
      throw new SkillInstructionFetchError(source.url, response.status, response.statusText);
    }
    return response.text();
  }

  throw new InvalidInstructionSourceError();
}

/**
 * Build SkillContent from metadata and resolved instructions.
 *
 * @param metadata - The skill metadata
 * @param instructions - The resolved instructions string
 * @param resolvedReferences - Optional resolved reference metadata from references/ directory
 * @returns The full skill content
 */
export function buildSkillContent(
  metadata: SkillMetadata,
  instructions: string,
  resolvedReferences?: SkillReferenceInfo[],
  resolvedExamples?: SkillExampleInfo[],
): SkillContent {
  // Append references routing table to instructions if references exist
  let finalInstructions = instructions;
  if (resolvedReferences && resolvedReferences.length > 0) {
    finalInstructions += buildReferencesTable(resolvedReferences);
  }
  if (resolvedExamples && resolvedExamples.length > 0) {
    finalInstructions += buildExamplesTable(resolvedExamples);
  }

  return {
    id: metadata.id ?? metadata.name,
    name: metadata.name,
    description: metadata.description,
    instructions: finalInstructions,
    tools: normalizeToolRefs(metadata.tools),
    parameters: metadata.parameters,
    examples: metadata.examples,
    license: metadata.license,
    compatibility: metadata.compatibility,
    specMetadata: metadata.specMetadata,
    allowedTools: metadata.allowedTools,
    resources: metadata.resources,
    resolvedReferences,
    resolvedExamples,
  };
}

/**
 * Build a markdown references routing table to append to skill instructions.
 * Follows the same format as SKILL.md scenario routing tables.
 */
function buildReferencesTable(refs: SkillReferenceInfo[]): string {
  const lines: string[] = ['', '', '## References', '', '| Reference | Description |', '| --------- | ----------- |'];

  for (const ref of refs) {
    lines.push(`| \`${ref.name}\` | ${ref.description} |`);
  }

  return lines.join('\n');
}

/**
 * Build a markdown examples table to append to skill instructions.
 * Groups examples by their parent reference.
 */
function buildExamplesTable(examples: SkillExampleInfo[]): string {
  const lines: string[] = [
    '',
    '',
    '## Examples',
    '',
    '| Example | Reference | Level | Description |',
    '| ------- | --------- | ----- | ----------- |',
  ];

  for (const ex of examples) {
    lines.push(`| \`${ex.name}\` | \`${ex.reference}\` | ${ex.level} | ${ex.description} |`);
  }

  return lines.join('\n');
}

/**
 * Scan an examples/ directory for .md files organized by reference subdirectories.
 * Parses YAML frontmatter for name, description, reference, and level.
 *
 * @param examplesDir - Absolute path to the examples/ directory
 * @returns Array of resolved example info, or undefined if no examples
 */
export async function resolveExamples(examplesDir: string): Promise<SkillExampleInfo[] | undefined> {
  if (!(await fileExists(examplesDir))) return undefined;

  let refDirs: string[];
  try {
    refDirs = (await readdir(examplesDir)).sort();
  } catch {
    return undefined;
  }

  const examples: SkillExampleInfo[] = [];

  for (const refDir of refDirs) {
    const refPath = `${examplesDir}/${refDir}`;
    try {
      const s = await stat(refPath);
      if (!s.isDirectory()) continue;
    } catch {
      continue;
    }

    let files: string[];
    try {
      files = (await readdir(refPath)).filter((f: string) => f.endsWith('.md')).sort();
    } catch {
      continue;
    }

    for (const file of files) {
      const content = await readFile(`${refPath}/${file}`, 'utf-8');
      const filenameWithoutExt = file.replace(/\.md$/, '');

      let name = filenameWithoutExt;
      let description = '';
      let reference = refDir;
      let level = 'basic';

      // Parse frontmatter
      const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
      if (fmMatch) {
        const fmLines = fmMatch[1].split(/\r?\n/);
        for (const line of fmLines) {
          const colonIdx = line.indexOf(':');
          if (colonIdx === -1) continue;
          const key = line.slice(0, colonIdx).trim();
          const val = line
            .slice(colonIdx + 1)
            .trim()
            .replace(/^["']|["']$/g, '');
          if (key === 'name' && val) name = val;
          if (key === 'description' && val) description = val;
          if (key === 'reference' && val) reference = val;
          if (key === 'level' && val) level = val;
        }
      }

      // Fallback description from first paragraph
      if (!description) {
        const body = fmMatch ? content.substring(content.indexOf('---', 3) + 3).trim() : content.trim();
        description = extractFirstParagraph(body);
      }

      examples.push({ name, description, reference, level, filename: `${refDir}/${file}` });
    }
  }

  return examples.length > 0 ? examples : undefined;
}

/**
 * Scan a references/ directory for .md files and extract metadata.
 * Parses YAML frontmatter for name/description, falls back to heading/paragraph.
 *
 * @param refsDir - Absolute path to the references/ directory
 * @returns Array of resolved reference info, or undefined if no references
 */
export async function resolveReferences(refsDir: string): Promise<SkillReferenceInfo[] | undefined> {
  if (!(await fileExists(refsDir))) return undefined;

  let files: string[];
  try {
    files = (await readdir(refsDir)).filter((f: string) => f.endsWith('.md')).sort();
  } catch {
    return undefined;
  }

  if (files.length === 0) return undefined;

  const refs: SkillReferenceInfo[] = [];
  for (const file of files) {
    const content = await readFile(`${refsDir}/${file}`, 'utf-8');
    const filenameWithoutExt = file.replace(/\.md$/, '');

    let name = filenameWithoutExt;
    let description = '';

    // Try parsing frontmatter
    const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
    if (fmMatch) {
      const fmLines = fmMatch[1].split(/\r?\n/);
      for (const line of fmLines) {
        const colonIdx = line.indexOf(':');
        if (colonIdx === -1) continue;
        const key = line.slice(0, colonIdx).trim();
        const val = line
          .slice(colonIdx + 1)
          .trim()
          .replace(/^["']|["']$/g, '');
        if (key === 'name' && val) name = val;
        if (key === 'description' && val) description = val;
      }
    }

    // Fallback: extract description from first paragraph
    if (!description) {
      const body = fmMatch ? content.substring(content.indexOf('---', 3) + 3).trim() : content.trim();
      description = extractFirstParagraph(body);
    }

    refs.push({ name, description, filename: file });
  }

  return refs;
}

/**
 * Extract the first non-empty paragraph after the heading from markdown content.
 */
function extractFirstParagraph(body: string): string {
  const lines = body.split(/\r?\n/);
  let foundHeading = false;
  const paragraphLines: string[] = [];

  for (const line of lines) {
    const trimmed = line.trim();
    if (!foundHeading && trimmed.startsWith('#')) {
      foundHeading = true;
      continue;
    }
    if (foundHeading) {
      if (trimmed === '') {
        if (paragraphLines.length > 0) break;
        continue;
      }
      if (trimmed.startsWith('#') || trimmed.startsWith('|') || trimmed.startsWith('-')) break;
      paragraphLines.push(trimmed);
    }
  }

  return paragraphLines.join(' ').slice(0, 200) || '';
}

/**
 * Normalize tool references to the array format expected by SkillContent.
 * Handles all supported tool reference formats: string, class, SkillToolRef, SkillToolRefWithClass.
 * Preserves the required flag for tool enforcement during skill sessions.
 */
function normalizeToolRefs(tools: SkillMetadata['tools']): SkillContent['tools'] {
  if (!tools) return [];

  return tools.map((tool) => {
    try {
      const normalized = normalizeToolRef(tool);
      return {
        name: normalized.name,
        purpose: normalized.purpose,
        required: normalized.required,
      };
    } catch {
      // Fallback for edge cases
      if (typeof tool === 'string') return { name: tool };
      if (typeof tool === 'function') return { name: tool.name ?? 'unknown' };
      if ('name' in (tool as object)) {
        const obj = tool as { name: string; purpose?: string; required?: boolean };
        return { name: obj.name, purpose: obj.purpose, required: obj.required };
      }
      if ('tool' in (tool as object)) {
        const obj = tool as { tool: { name?: string }; purpose?: string; required?: boolean };
        return { name: obj.tool.name ?? 'unknown', purpose: obj.purpose, required: obj.required };
      }
      return { name: 'unknown' };
    }
  });
}

/**
 * Format a skill for LLM consumption.
 *
 * Creates a markdown-formatted string with the skill instructions,
 * tool information, and any warnings about missing tools.
 *
 * @param skill - The loaded skill content
 * @param availableTools - List of available tools
 * @param missingTools - List of missing tools
 * @returns Formatted skill content as markdown
 */
export function formatSkillForLLM(skill: SkillContent, availableTools: string[], missingTools: string[]): string {
  const parts: string[] = [];

  // Header
  parts.push(`# Skill: ${skill.name}`);
  parts.push('');
  parts.push(skill.description);
  parts.push('');

  // License and compatibility info
  if (skill.license) {
    parts.push(`**License:** ${skill.license}`);
    parts.push('');
  }
  if (skill.compatibility) {
    parts.push(`**Compatibility:** ${skill.compatibility}`);
    parts.push('');
  }

  // Warning if tools are missing
  if (missingTools.length > 0) {
    parts.push('> **Warning:** Some tools referenced by this skill are not available:');
    parts.push(`> Missing: ${missingTools.join(', ')}`);
    parts.push('> You may need to adapt the workflow or skip steps that require these tools.');
    parts.push('');
  }

  // Tools section
  if (skill.tools.length > 0) {
    parts.push('## Tools');
    parts.push('');
    for (const tool of skill.tools) {
      const status = availableTools.includes(tool.name) ? '✓' : '✗';
      const purpose = tool.purpose ? ` - ${tool.purpose}` : '';
      parts.push(`- [${status}] \`${tool.name}\`${purpose}`);
    }
    parts.push('');
  }

  // Parameters section
  if (skill.parameters && skill.parameters.length > 0) {
    parts.push('## Parameters');
    parts.push('');
    for (const param of skill.parameters) {
      const required = param.required ? ' (required)' : '';
      const desc = param.description ? `: ${param.description}` : '';
      parts.push(`- **${param.name}**${required}${desc}`);
    }
    parts.push('');
  }

  // Instructions
  parts.push('## Instructions');
  parts.push('');
  parts.push(skill.instructions);

  // Examples section
  if (skill.examples && skill.examples.length > 0) {
    parts.push('');
    parts.push('## Examples');
    parts.push('');
    for (const example of skill.examples) {
      parts.push(`### ${example.scenario}`);
      if (example.expectedOutcome) {
        parts.push(`Expected outcome: ${example.expectedOutcome}`);
      }
      parts.push('');
    }
  }

  return parts.join('\n');
}

/**
 * Generate next steps guidance after loading skills.
 *
 * @param skills - Array of loaded skills with their tool availability
 * @param allToolsAvailable - Whether all tools across all skills are available
 * @returns Human-readable guidance string
 */
export function generateNextSteps(
  skills: Array<{ name: string; isComplete: boolean; tools: Array<{ name: string; available: boolean }> }>,
  allToolsAvailable: boolean,
): string {
  if (skills.length === 0) {
    return 'No skills were loaded. Try reading the skill://index.json resource to find available skills.';
  }

  if (!allToolsAvailable) {
    const missingToolSkills = skills.filter((s) => !s.isComplete);
    return (
      `Some tools are missing for: ${missingToolSkills.map((s) => s.name).join(', ')}. ` +
      'You can still follow the instructions but may need to skip or adapt steps that use unavailable tools. ' +
      'Check the tools[] array in each skill to see which tools are available.'
    );
  }

  if (skills.length === 1) {
    const skill = skills[0];
    return (
      `Ready to execute "${skill.name}". ` +
      'Read the formattedContent for step-by-step instructions. ' +
      `The skill uses ${skill.tools.length} tool(s) - see their schemas in tools[].inputSchema to understand the expected parameters.`
    );
  }

  return (
    `Loaded ${skills.length} skills. Review each skill's instructions in formattedContent. ` +
    'You can combine workflows by following instructions from multiple skills. ' +
    'Check tools[].inputSchema for each tool to understand the expected parameters.'
  );
}

/**
 * Generate search guidance based on results.
 *
 * @param skills - Array of search results with executability info
 * @param query - The original search query
 * @returns Human-readable guidance string
 */
export function generateSearchGuidance(
  skills: Array<{ name: string; score: number; canExecute: boolean }>,
  query: string,
): string {
  if (skills.length === 0) {
    return (
      `No skills found for "${query}". ` +
      'Try a different search query, or the MCP server may not have skills for this task. ' +
      'You can still use individual tools directly - check the available tools list.'
    );
  }

  const executableSkills = skills.filter((s) => s.canExecute);
  const topSkill = skills[0];

  if (executableSkills.length > 0) {
    const topExecutable = executableSkills[0];
    return (
      `Found ${skills.length} skill(s). Recommended: read the skill://${topExecutable.name}/SKILL.md resource ` +
      `to get full instructions. ${executableSkills.length} skill(s) have all tools available.`
    );
  }

  return (
    `Found ${skills.length} skill(s), but some tools are missing. ` +
    `Try reading the skill://${topSkill.name}/SKILL.md resource to see which tools are available. ` +
    'You may be able to partially execute the workflow.'
  );
}
