import { normalize, isAbsolute, sep } from 'path';
import type {
  Block,
  BlockContent,
  PrettierMarkdownOptions,
  Program,
  Value,
} from '@promptscript/core';
import { DEFAULT_PRETTIER_OPTIONS } from '@promptscript/core';
import { ConventionRenderer } from './convention-renderer.js';
import { StandardsExtractor } from './extractors/index.js';
import type { FormatOptions, Formatter, FormatterOutput } from './types.js';

/**
 * Abstract base formatter with common helper methods.
 * Extend this class to create new formatter implementations.
 */
export abstract class BaseFormatter implements Formatter {
  abstract readonly name: string;
  abstract readonly outputPath: string;
  abstract readonly description: string;
  abstract readonly defaultConvention: string;
  abstract format(ast: Program, options?: FormatOptions): FormatterOutput;

  /**
   * Shared standards extractor for consistent extraction across all formatters.
   */
  protected readonly standardsExtractor = new StandardsExtractor();

  /**
   * Create a convention renderer for this formatter.
   * Uses the provided convention from options or falls back to the default.
   */
  protected createRenderer(options?: FormatOptions): ConventionRenderer {
    const convention = options?.convention ?? this.defaultConvention;
    return new ConventionRenderer({
      convention,
      prettier: options?.prettier,
    });
  }

  /**
   * Get resolved Prettier options, merging provided options with defaults.
   */
  protected getPrettierOptions(options?: FormatOptions): Required<PrettierMarkdownOptions> {
    return {
      ...DEFAULT_PRETTIER_OPTIONS,
      ...options?.prettier,
    };
  }

  /**
   * Get the output path, respecting options override.
   */
  protected getOutputPath(options?: FormatOptions): string {
    return options?.outputPath ?? this.outputPath;
  }

  /**
   * Find a block by name, ignoring internal blocks (starting with __).
   */
  protected findBlock(ast: Program, name: string): Block | undefined {
    return ast.blocks.find((b) => b.name === name && !b.name.startsWith('__'));
  }

  /**
   * Extract text from block content.
   */
  protected extractText(content: BlockContent): string {
    switch (content.type) {
      case 'TextContent':
        return content.value.trim();
      case 'MixedContent':
        return content.text?.value.trim() ?? '';
      default:
        return '';
    }
  }

  /**
   * Get a specific property from block content.
   */
  protected getProp(content: BlockContent, key: string): Value | undefined {
    switch (content.type) {
      case 'ObjectContent':
        return content.properties[key];
      case 'MixedContent':
        return content.properties[key];
      default:
        return undefined;
    }
  }

  /**
   * Get all properties from block content.
   */
  protected getProps(content: BlockContent): Record<string, Value> {
    switch (content.type) {
      case 'ObjectContent':
        return content.properties;
      case 'MixedContent':
        return content.properties;
      default:
        return {};
    }
  }

  /**
   * Format standards list from array of values (pass-through).
   * Returns array of strings for rendering as bullet list.
   */
  protected formatStandardsList(items: Value): string[] {
    if (!Array.isArray(items)) return [];
    return items.map((item) => this.valueToString(item)).filter((s) => s.length > 0);
  }

  /**
   * Format an array as comma-separated string.
   */
  protected formatArray(arr: unknown[]): string {
    return arr.map(String).join(', ');
  }

  /**
   * Truncate string to max length with ellipsis.
   */
  protected truncate(str: string, max: number): string {
    return str.length > max ? str.substring(0, max - 3) + '...' : str;
  }

  /**
   * Get meta field value as string.
   */
  protected getMetaField(ast: Program, key: string): string | undefined {
    const value = ast.meta?.fields?.[key];
    if (typeof value === 'string') return value;
    if (typeof value === 'number') return String(value);
    return undefined;
  }

  /**
   * Extract array elements from block content.
   */
  protected getArrayElements(content: BlockContent): Value[] {
    if (content.type === 'ArrayContent') {
      return content.elements;
    }
    return [];
  }

  /**
   * Convert value to string representation.
   */
  protected valueToString(value: Value): string {
    if (value === null) return '';
    if (typeof value === 'string') return value;
    if (typeof value === 'number' || typeof value === 'boolean') {
      return String(value);
    }
    if (Array.isArray(value)) {
      return value.map((v) => this.valueToString(v)).join(', ');
    }
    if (typeof value === 'object' && 'type' in value) {
      if (value.type === 'TextContent' && typeof value.value === 'string') {
        return value.value.trim();
      }
    }
    return '';
  }

  /**
   * Safe extraction of a section that contains a header + content + code block + content
   * Avoids ReDoS by using string search instead of backtracking regex.
   * Matches pattern: Header ... ``` ... ```
   */
  protected extractSectionWithCodeBlock(text: string, header: string): string | null {
    const headerIndex = text.indexOf(header);
    if (headerIndex === -1) return null;

    // Start searching after the header
    const offset = headerIndex + header.length;

    const startCodeBlock = text.indexOf('```', offset);
    if (startCodeBlock === -1) return null;

    const endCodeBlock = text.indexOf('```', startCodeBlock + 3);
    if (endCodeBlock === -1) return null;

    // Include the closing backticks (length 3)
    const endPos = endCodeBlock + 3;

    return text.substring(headerIndex, endPos);
  }

  /**
   * Normalize markdown content to match Prettier formatting.
   * - Strips common leading indentation from lines
   * - Trims trailing whitespace from lines
   * - Normalizes markdown table formatting
   * - Adds blank lines before lists when preceded by text
   * - Adds blank lines before code blocks when preceded by text
   * - Escapes markdown special characters in paths
   */
  protected normalizeMarkdownForPrettier(content: string): string {
    const lines = content.split('\n');

    // Find common leading indentation (minimum non-empty line indent)
    // Include code block markers in calculation to handle content that's mainly code blocks
    let minIndent = Infinity;
    let inCodeBlock = false;
    for (const line of lines) {
      const trimmed = line.trimEnd();
      // Check for code block markers (need trimStart to handle indented markers)
      if (trimmed.trimStart().startsWith('```')) {
        inCodeBlock = !inCodeBlock;
        // Include code block markers in minIndent calculation
        if (trimmed.length > 0) {
          const match = line.match(/^(\s*)/);
          const leadingSpaces = match?.[1]?.length ?? 0;
          minIndent = Math.min(minIndent, leadingSpaces);
        }
        continue;
      }
      if (inCodeBlock) continue;
      if (trimmed.length === 0) continue;
      const match = line.match(/^(\s*)/);
      const leadingSpaces = match?.[1]?.length ?? 0;
      minIndent = Math.min(minIndent, leadingSpaces);
    }
    if (minIndent === Infinity) minIndent = 0;

    // Process lines
    const result: string[] = [];
    let inTable = false;
    let tableLines: string[] = [];
    inCodeBlock = false;

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

      // Strip common indentation from all lines
      const unindentedLine = minIndent > 0 ? trimmedLine.slice(minIndent) : trimmedLine;

      // Check for code block start BEFORE toggling inCodeBlock
      const isCodeBlockMarker = trimmedLine.trimStart().startsWith('```');
      const isCodeBlockStart = isCodeBlockMarker && !inCodeBlock;

      // Toggle code block state
      if (isCodeBlockMarker) {
        inCodeBlock = !inCodeBlock;
      }

      // Inside code blocks, just use unindented line
      if (inCodeBlock || unindentedLine.startsWith('```')) {
        // Add blank line before code block if previous line is non-empty text
        if (isCodeBlockStart) {
          const prevLine = result.length > 0 ? (result[result.length - 1] ?? '') : '';
          if (prevLine.trim() && !prevLine.startsWith('#')) {
            result.push('');
          }
        }
        result.push(unindentedLine);
        continue;
      }

      let processedLine = unindentedLine;

      // Escape markdown special characters in path-like content (outside code blocks)
      // Only escape __name__ outside of backticks
      // __name__ → \_\_name\_\_ (prevents bold)
      processedLine = processedLine.replace(/__([^_]+)__/g, '\\_\\_$1\\_\\_');
      // Escape * in glob patterns (like packages/* or .cursor/rules/*.md) outside backticks
      processedLine = this.escapeGlobAsteriskOutsideBackticks(processedLine);

      // Check for blank line after headers BEFORE table detection
      const prevLine = result.length > 0 ? result[result.length - 1] : '';
      const isHeader = prevLine?.trimStart().startsWith('#');

      // Add blank line after header if content follows
      if (isHeader && processedLine.trim()) {
        result.push('');
      }

      // Detect table rows (lines starting with |)
      if (processedLine.trimStart().startsWith('|') && processedLine.trimEnd().endsWith('|')) {
        inTable = true;
        tableLines.push(processedLine.trim());
      } else {
        // If we were in a table, format and flush it
        if (inTable && tableLines.length > 0) {
          result.push(...this.formatMarkdownTable(tableLines));
          tableLines = [];
          inTable = false;
        }

        const isListItem = processedLine.trimStart().startsWith('- ');
        const isNumberedItem = /^\d+\.\s/.test(processedLine.trimStart());
        const prevLineTrimmed = prevLine?.trimStart() ?? '';
        const isPrevListItem = prevLineTrimmed.startsWith('- ') || /^\d+\.\s/.test(prevLineTrimmed);
        // Check if previous line ends with colon (like "Security mindset:")
        const prevEndsWithColon = prevLine?.trimEnd().endsWith(':') ?? false;

        // Add blank line before list item if:
        // - previous line was non-empty text that's not a list item, header, or blank
        // - OR previous line ends with colon (like "Security mindset:")
        if ((isListItem || isNumberedItem) && prevLine && !isPrevListItem && !isHeader) {
          result.push('');
        } else if ((isListItem || isNumberedItem) && prevEndsWithColon) {
          result.push('');
        }

        result.push(processedLine);
      }
    }

    // Handle table at end of content
    if (tableLines.length > 0) {
      result.push(...this.formatMarkdownTable(tableLines));
    }

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

  /**
   * Escape glob asterisks (like packages/* or .cursor/rules/*.md) outside of backticks.
   * Prettier escapes these to prevent them from being interpreted as emphasis markers.
   */
  private escapeGlobAsteriskOutsideBackticks(line: string): string {
    // Split by backticks to identify code spans
    const parts = line.split('`');
    return parts
      .map((part, index) => {
        // Even indices are outside backticks, odd indices are inside
        if (index % 2 === 0) {
          // Escape * when preceded by / (glob patterns like packages/* or .cursor/rules/*.md)
          return part.replace(/\/\*/g, '/\\*');
        }
        return part;
      })
      .join('`');
  }

  /**
   * Strip all leading indentation from markdown content.
   * Used for AGENTS.md where content from multiple sources has inconsistent indentation.
   * Preserves indentation inside code blocks.
   */
  protected stripAllIndent(content: string): string {
    const lines = content.split('\n');
    const result: string[] = [];
    let inCodeBlock = false;

    for (const line of lines) {
      const trimmedEnd = line.trimEnd();

      // Track code blocks
      if (trimmedEnd.trimStart().startsWith('```')) {
        // Add blank line before code block if previous line was non-empty
        if (!inCodeBlock) {
          const prevLine = result.length > 0 ? (result[result.length - 1] ?? '') : '';
          if (prevLine && prevLine.trim()) {
            result.push('');
          }
        }
        inCodeBlock = !inCodeBlock;
        // Strip indent from code block markers too
        result.push(trimmedEnd.trimStart());
        continue;
      }

      // Inside code blocks, preserve relative indentation
      if (inCodeBlock) {
        result.push(trimmedEnd);
        continue;
      }

      // Outside code blocks, strip all leading whitespace
      let stripped = trimmedEnd.trimStart();

      // Escape markdown special characters for Prettier compatibility
      // Escape __ to \_\_ (to avoid emphasis parsing)
      stripped = stripped.replace(/__/g, '\\_\\_');
      // Escape * in glob patterns outside backticks
      stripped = this.escapeGlobAsteriskOutsideBackticks(stripped);

      // Add blank line before list item if previous line was non-empty and not a list
      const prevLine = result.length > 0 ? (result[result.length - 1] ?? '') : '';
      const isListItem = stripped.startsWith('- ') || /^\d+\.\s/.test(stripped);
      const isPrevList = prevLine.startsWith('- ') || /^\d+\.\s/.test(prevLine);
      const prevEndsWithColon = prevLine.trimEnd().endsWith(':');
      if (isListItem && prevLine && !isPrevList) {
        result.push('');
      } else if (isListItem && prevEndsWithColon) {
        result.push('');
      }

      result.push(stripped);
    }

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

  /**
   * Format a markdown table to match Prettier output.
   * Prettier removes trailing whitespace from cells.
   */
  private formatMarkdownTable(tableLines: string[]): string[] {
    if (tableLines.length === 0) return [];

    // Parse table into cells
    const rows = tableLines.map((line) =>
      line
        .split('|')
        .slice(1, -1) // Remove empty first/last from split
        .map((cell) => cell.trim())
    );

    // Calculate column widths (minimum width for content)
    const colCount = rows[0]?.length ?? 0;
    const colWidths: number[] = new Array(colCount).fill(0);

    for (const row of rows) {
      for (let i = 0; i < row.length; i++) {
        const cell = row[i] ?? '';
        // For separator row, use 3 as minimum (---)
        const width = cell.match(/^-+$/) ? 3 : cell.length;
        colWidths[i] = Math.max(colWidths[i] ?? 0, width);
      }
    }

    // Rebuild table with proper formatting
    return rows.map((row) => {
      const cells = row.map((cell, colIndex) => {
        const width = colWidths[colIndex] ?? 0;
        // Separator row uses dashes
        if (cell.match(/^-+$/)) {
          return '-'.repeat(width);
        }
        // Pad cell content
        return cell.padEnd(width);
      });
      return '| ' + cells.join(' | ') + ' |';
    });
  }

  /**
   * Remove common leading whitespace from all lines (dedent).
   * Handles the case where trim() was already called, causing the first line
   * to lose its indentation while subsequent lines retain theirs.
   * Calculates minimum indent from lines 2+ only.
   */
  protected dedent(text: string): string {
    const lines = text.split('\n');
    if (lines.length <= 1) return text.trim();

    // Calculate minimum indent from lines 2+ only
    const minIndent = lines
      .slice(1)
      .filter((line) => line.trim().length > 0)
      .reduce((min, line) => {
        const match = line.match(/^(\s*)/);
        const indent = match?.[1]?.length ?? 0;
        return Math.min(min, indent);
      }, Infinity);

    if (minIndent === 0 || minIndent === Infinity) {
      return text.trim();
    }

    const firstLine = lines[0] ?? '';
    return [
      firstLine.trim(),
      ...lines.slice(1).map((line) => (line.trim().length > 0 ? line.slice(minIndent) : '')),
    ]
      .join('\n')
      .trim();
  }

  /**
   * Normalize a user-provided output directory (from `@use ... into "<path>"`
   * or `skillTargets` config) to a safe forward-slash relative path. Rejects
   * `..`, `.` and leading slashes so the result can be appended to a target's
   * dot-directory without escaping it.
   */
  protected normalizeOutputDir(dir: string): string {
    return dir
      .replace(/\\/g, '/')
      .replace(/^\/+/, '')
      .replace(/\/+$/g, '')
      .split('/')
      .filter((segment) => segment.length > 0 && segment !== '.' && segment !== '..')
      .join('/');
  }

  /**
   * Return true when the target configuration allows emitting the given skill.
   */
  protected shouldIncludeSkill(name: string, options?: FormatOptions): boolean {
    const includeSkills = options?.targetConfig?.includeSkills;
    if (includeSkills === false) return false;
    if (Array.isArray(includeSkills)) return includeSkills.includes(name);
    return true;
  }

  /**
   * Resolve the directory for a generated skill, respecting per-target skill
   * base overrides while preserving existing `@use ... into` behavior.
   */
  protected resolveSkillDir(
    defaultSkillBasePath: string,
    skillName: string,
    outputDir?: string,
    options?: FormatOptions
  ): string {
    const configuredBaseDir = options?.targetConfig?.skillBaseDir;
    const skillBasePath = configuredBaseDir
      ? this.normalizeOutputDir(configuredBaseDir)
      : defaultSkillBasePath;

    if (!outputDir) {
      return `${skillBasePath}/${skillName}`;
    }

    const normalizedOutputDir = this.normalizeOutputDir(outputDir);
    if (!configuredBaseDir) {
      const targetRoot = defaultSkillBasePath.replace(/\/skills$/, '');
      return `${targetRoot}/${normalizedOutputDir}`;
    }

    const outputSegments = normalizedOutputDir.split('/').filter((segment) => segment.length > 0);
    const relativeOutputDir =
      outputSegments[0] === 'skills' ? outputSegments.slice(1).join('/') : outputSegments.join('/');

    return relativeOutputDir
      ? `${skillBasePath}/${relativeOutputDir}`
      : `${skillBasePath}/${skillName}`;
  }

  /**
   * Filter resource files to only include safe paths.
   * Rejects paths with traversal, absolute paths, and unsafe names.
   */
  protected sanitizeResourceFiles(
    resources: Array<{ relativePath: string; content: string }> | undefined,
    targetDir: string
  ): FormatterOutput[] {
    if (!resources || resources.length === 0) return [];

    return resources
      .filter((r) => {
        const normalized = normalize(r.relativePath);
        if (isAbsolute(normalized)) return false;
        const segments = normalized.split(sep);
        return !segments.some((s) => s === '..');
      })
      .map((r) => ({
        path: `${targetDir}/${r.relativePath}`,
        content: r.content,
      }));
  }

  /**
   * Extract example entries from a properties object.
   * Shared iteration logic used by both extractExamples and extractSkillExamples.
   */
  private extractExamplesFromProps(
    props: Record<string, Value>
  ): Array<{ name: string; input: string; output: string; description?: string }> {
    const examples: Array<{ name: string; input: string; output: string; description?: string }> =
      [];

    for (const [name, value] of Object.entries(props)) {
      if (!value || typeof value !== 'object' || Array.isArray(value)) continue;

      const obj = value as Record<string, Value>;
      const input = obj['input'] ? this.valueToString(obj['input']) : '';
      const output = obj['output'] ? this.valueToString(obj['output']) : '';

      if (!input || !output) continue;

      const description = obj['description'] ? this.valueToString(obj['description']) : undefined;
      examples.push({ name, input, output, description });
    }

    return examples;
  }

  /**
   * Extract examples from the @examples block.
   * Returns an array of example definitions with name, input, output, and optional description.
   */
  protected extractExamples(
    ast: Program
  ): Array<{ name: string; input: string; output: string; description?: string }> {
    const block = this.findBlock(ast, 'examples');
    if (!block) return [];

    return this.extractExamplesFromProps(this.getProps(block.content));
  }

  /**
   * Extract examples from a skill's nested examples property.
   * Returns the same shape as extractExamples.
   */
  protected extractSkillExamples(
    skillProps: Record<string, Value>
  ): Array<{ name: string; input: string; output: string; description?: string }> {
    const examplesValue = skillProps['examples'];
    if (!examplesValue || typeof examplesValue !== 'object' || Array.isArray(examplesValue))
      return [];

    return this.extractExamplesFromProps(examplesValue as Record<string, Value>);
  }

  /**
   * Render an examples section from the @examples block.
   * Shared rendering logic used by Claude, GitHub, and MarkdownInstructionFormatter.
   *
   * @param sectionName - Custom section heading name (default: 'Examples')
   */
  protected renderExamplesSection(
    ast: Program,
    renderer: ConventionRenderer,
    sectionName = 'Examples'
  ): string | null {
    const examples = this.extractExamples(ast);
    if (examples.length === 0) return null;

    const parts: string[] = [];

    for (const example of examples) {
      parts.push(`### Example: ${example.name}`);
      if (example.description) {
        const safeDescription = example.description.replace(/[\r\n]+/g, ' ').trim();
        parts.push('');
        parts.push(safeDescription);
      }
      parts.push('');
      parts.push('**Input:**');
      parts.push('');
      parts.push(this.renderCodeFence(this.dedent(example.input)));
      parts.push('');
      parts.push('**Output:**');
      parts.push('');
      parts.push(this.renderCodeFence(this.dedent(example.output)));
    }

    const content = parts.join('\n');
    return renderer.renderSection(sectionName, content) + '\n';
  }

  /** Base path for skills, or null if formatter has no skill support. */
  getSkillBasePath(): string | null {
    return null;
  }

  /** Skill file name, or null if formatter has no skill support. */
  getSkillFileName(): string | null {
    return null;
  }

  /**
   * How this formatter handles skill references.
   * - 'directory': emit as separate files in references/ subdirectory
   * - 'inline': append as sections in the main output file
   * - 'none': references not supported
   */
  referencesMode(): 'directory' | 'inline' | 'none' {
    return 'none';
  }

  /**
   * Default pass-through for injected skill content. Formatters whose target
   * tools restrict skill frontmatter (e.g. Factory AI) override this hook to
   * filter unsupported fields before the compiler writes the file.
   */
  transformInjectedSkillContent(content: string): string {
    return content;
  }

  /**
   * Generate a provenance comment for a reference file.
   */
  protected referenceProvenance(sourcePath: string): string {
    return `<!-- from: ${sourcePath} -->`;
  }

  /**
   * Check if a skill name is safe for use in file paths.
   */
  protected isSafeSkillName(name: string): boolean {
    return !name.includes('..') && !name.includes('/') && !name.includes('\\');
  }

  /**
   * Render content inside a code fence, using a longer fence if the content
   * itself contains triple backticks (prevents code fence injection).
   */
  protected renderCodeFence(content: string, lang = ''): string {
    const fence = content.includes('```') ? '````' : '```';
    return `${fence}${lang}\n${content}\n${fence}`;
  }
}
