"""Prompt linter — static analysis and best practices for LLM prompts."""

import re
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Dict, List, Optional

from promptkit.prompt import Prompt, PromptVersion


class Severity(str, Enum):
    """Lint severity levels."""
    ERROR = "error"
    WARNING = "warning"
    INFO = "info"


@dataclass
class LintResult:
    """A single lint finding."""
    rule: str
    severity: Severity
    message: str
    line: Optional[int] = None
    suggestion: Optional[str] = None

    def to_dict(self) -> Dict[str, Any]:
        return {
            "rule": self.rule,
            "severity": self.severity.value,
            "message": self.message,
            "line": self.line,
            "suggestion": self.suggestion,
        }


@dataclass
class LintReport:
    """Complete lint report for a prompt."""
    prompt_name: str
    version: int
    results: List[LintResult] = field(default_factory=list)
    score: float = 100.0

    @property
    def errors(self) -> List[LintResult]:
        return [r for r in self.results if r.severity == Severity.ERROR]

    @property
    def warnings(self) -> List[LintResult]:
        return [r for r in self.results if r.severity == Severity.WARNING]

    @property
    def infos(self) -> List[LintResult]:
        return [r for r in self.results if r.severity == Severity.INFO]

    @property
    def passed(self) -> bool:
        return len(self.errors) == 0

    def to_dict(self) -> Dict[str, Any]:
        return {
            "prompt_name": self.prompt_name,
            "version": self.version,
            "results": [r.to_dict() for r in self.results],
            "score": self.score,
            "passed": self.passed,
            "summary": {
                "errors": len(self.errors),
                "warnings": len(self.warnings),
                "info": len(self.infos),
            },
        }


class PromptLinter:
    """Lints prompts for quality, best practices, and common issues."""

    # Default rules — all enabled
    DEFAULT_RULES = {
        "no-empty": True,
        "min-length": True,
        "max-length": True,
        "no-trailing-whitespace": True,
        "no-double-spaces": True,
        "has-instruction": True,
        "no-typo-variables": True,
        "balanced-braces": True,
        "no-todo": True,
        "has-context": True,
        "no-ambiguity": True,
        "consistent-format": True,
    }

    # Configurable thresholds
    DEFAULT_THRESHOLDS = {
        "min_length": 10,
        "max_length": 50000,
        "max_line_length": 500,
    }

    def __init__(
        self,
        rules: Optional[Dict[str, bool]] = None,
        thresholds: Optional[Dict[str, int]] = None,
    ):
        self.rules = {**self.DEFAULT_RULES, **(rules or {})}
        self.thresholds = {**self.DEFAULT_THRESHOLDS, **(thresholds or {})}

    def lint(self, prompt: Prompt, version: Optional[int] = None) -> LintReport:
        """Lint a prompt and return a report."""
        ver = prompt.get_version(version)
        if ver is None:
            ver = prompt.latest
        if ver is None:
            report = LintReport(prompt_name=prompt.name, version=0)
            report.results.append(LintResult(
                rule="no-empty",
                severity=Severity.ERROR,
                message="Prompt has no versions",
            ))
            report.score = 0.0
            return report

        return self.lint_content(ver.content, prompt.name, ver.version)

    def lint_content(self, content: str, name: str = "unnamed", version: int = 1) -> LintReport:
        """Lint raw prompt content."""
        report = LintReport(prompt_name=name, version=version)

        checks = [
            ("no-empty", self._check_empty),
            ("min-length", self._check_min_length),
            ("max-length", self._check_max_length),
            ("no-trailing-whitespace", self._check_trailing_whitespace),
            ("no-double-spaces", self._check_double_spaces),
            ("has-instruction", self._check_has_instruction),
            ("no-typo-variables", self._check_typo_variables),
            ("balanced-braces", self._check_balanced_braces),
            ("no-todo", self._check_no_todo),
            ("has-context", self._check_has_context),
            ("no-ambiguity", self._check_no_ambiguity),
            ("consistent-format", self._check_consistent_format),
        ]

        for rule_name, check_fn in checks:
            if self.rules.get(rule_name, True):
                results = check_fn(content)
                report.results.extend(results)

        # Calculate score: -20 per error, -5 per warning, -1 per info
        penalty = (
            len(report.errors) * 20
            + len(report.warnings) * 5
            + len(report.infos) * 1
        )
        report.score = max(0.0, 100.0 - penalty)

        return report

    def _check_empty(self, content: str) -> List[LintResult]:
        if not content or not content.strip():
            return [LintResult(
                rule="no-empty",
                severity=Severity.ERROR,
                message="Prompt content is empty",
                suggestion="Add meaningful instructions to your prompt",
            )]
        return []

    def _check_min_length(self, content: str) -> List[LintResult]:
        min_len = self.thresholds["min_length"]
        if len(content.strip()) < min_len:
            return [LintResult(
                rule="min-length",
                severity=Severity.WARNING,
                message=f"Prompt is very short ({len(content.strip())} chars, min: {min_len})",
                suggestion="Consider adding more context or instructions",
            )]
        return []

    def _check_max_length(self, content: str) -> List[LintResult]:
        max_len = self.thresholds["max_length"]
        if len(content) > max_len:
            return [LintResult(
                rule="max-length",
                severity=Severity.WARNING,
                message=f"Prompt is very long ({len(content)} chars, max: {max_len})",
                suggestion="Consider splitting into smaller, focused prompts",
            )]
        return []

    def _check_trailing_whitespace(self, content: str) -> List[LintResult]:
        results = []
        for i, line in enumerate(content.splitlines(), 1):
            if line != line.rstrip():
                results.append(LintResult(
                    rule="no-trailing-whitespace",
                    severity=Severity.INFO,
                    message=f"Trailing whitespace on line {i}",
                    line=i,
                    suggestion="Remove trailing whitespace",
                ))
        # Limit to first 5 occurrences
        if len(results) > 5:
            count = len(results)
            results = results[:5]
            results.append(LintResult(
                rule="no-trailing-whitespace",
                severity=Severity.INFO,
                message=f"... and {count - 5} more trailing whitespace issues",
            ))
        return results

    def _check_double_spaces(self, content: str) -> List[LintResult]:
        results = []
        for i, line in enumerate(content.splitlines(), 1):
            # Skip lines that are clearly formatting (code blocks, tables)
            stripped = line.strip()
            if stripped.startswith("|") or stripped.startswith("```"):
                continue
            if "  " in line and not line.startswith("  "):
                results.append(LintResult(
                    rule="no-double-spaces",
                    severity=Severity.INFO,
                    message=f"Double space found on line {i}",
                    line=i,
                    suggestion="Use single spaces between words",
                ))
        if len(results) > 3:
            count = len(results)
            results = results[:3]
            results.append(LintResult(
                rule="no-double-spaces",
                severity=Severity.INFO,
                message=f"... and {count - 3} more double-space issues",
            ))
        return results

    def _check_has_instruction(self, content: str) -> List[LintResult]:
        """Check if prompt contains clear instructions."""
        instruction_patterns = [
            r'\b(you are|you\'re|act as|behave as|your role)\b',
            r'\b(please|must|should|always|never|ensure|make sure)\b',
            r'\b(respond|answer|generate|create|write|output|return)\b',
            r'\b(format|structure|style|tone|language)\b',
            r'\b(step \d|first|then|finally|instructions?)\b',
        ]
        lower = content.lower()
        matches = sum(1 for p in instruction_patterns if re.search(p, lower))
        if matches == 0:
            return [LintResult(
                rule="has-instruction",
                severity=Severity.WARNING,
                message="No clear instructions detected",
                suggestion="Add explicit instructions (e.g., 'You are...', 'Please...', 'Generate...')",
            )]
        return []

    def _check_typo_variables(self, content: str) -> List[LintResult]:
        """Check for likely typos in Jinja2 variables."""
        results = []
        # Check for single-brace variables (likely typo)
        single_brace = re.findall(r'(?<!\{)\{(\w+)\}(?!\})', content)
        for var in single_brace:
            # Skip if it looks like a Python/JSON format string
            if var in ("}", "{"):
                continue
            results.append(LintResult(
                rule="no-typo-variables",
                severity=Severity.WARNING,
                message=f"Single-brace variable '{{{var}}}' — did you mean '{{{{  {var}  }}}}'?",
                suggestion=f"Use double braces for Jinja2: {{{{ {var} }}}}",
            ))
        return results

    def _check_balanced_braces(self, content: str) -> List[LintResult]:
        """Check for unbalanced Jinja2 braces."""
        results = []
        open_double = content.count("{{")
        close_double = content.count("}}")
        if open_double != close_double:
            results.append(LintResult(
                rule="balanced-braces",
                severity=Severity.ERROR,
                message=f"Unbalanced braces: {open_double} '{{{{' vs {close_double} '}}}}'",
                suggestion="Ensure every '{{' has a matching '}}'",
            ))

        # Check for {% %} blocks
        open_block = content.count("{%")
        close_block = content.count("%}")
        if open_block != close_block:
            results.append(LintResult(
                rule="balanced-braces",
                severity=Severity.ERROR,
                message=f"Unbalanced block tags: {open_block} '{{%' vs {close_block} '%}}'",
                suggestion="Ensure every '{%' has a matching '%}'",
            ))

        return results

    def _check_no_todo(self, content: str) -> List[LintResult]:
        """Check for TODO/FIXME/HACK markers."""
        results = []
        markers = ["TODO", "FIXME", "HACK", "XXX", "PLACEHOLDER"]
        for i, line in enumerate(content.splitlines(), 1):
            for marker in markers:
                if marker in line.upper():
                    results.append(LintResult(
                        rule="no-todo",
                        severity=Severity.WARNING,
                        message=f"'{marker}' found on line {i}",
                        line=i,
                        suggestion=f"Resolve the {marker} before using in production",
                    ))
        return results

    def _check_has_context(self, content: str) -> List[LintResult]:
        """Check if prompt provides enough context for the LLM."""
        lines = [l.strip() for l in content.splitlines() if l.strip()]
        word_count = len(content.split())
        if word_count < 5:
            return [LintResult(
                rule="has-context",
                severity=Severity.WARNING,
                message=f"Very little context ({word_count} words)",
                suggestion="Provide more context — what, why, constraints, examples",
            )]
        return []

    def _check_no_ambiguity(self, content: str) -> List[LintResult]:
        """Check for vague or ambiguous language."""
        results = []
        ambiguous = [
            (r'\b(maybe|perhaps|possibly|might want to)\b', "Ambiguous language"),
            (r'\b(good|nice|great|bad)\b(?!\s+(at|with|for))',
             "Vague quality descriptor"),
            (r'\b(stuff|things|etc\.?)\b', "Vague reference"),
            (r'\b(do something|something like|kind of|sort of)\b',
             "Imprecise instruction"),
        ]
        lower = content.lower()
        for pattern, desc in ambiguous:
            matches = re.findall(pattern, lower)
            if matches:
                results.append(LintResult(
                    rule="no-ambiguity",
                    severity=Severity.INFO,
                    message=f"{desc}: found '{matches[0]}' — be more specific",
                    suggestion="Use precise language for clearer LLM responses",
                ))
        return results

    def _check_consistent_format(self, content: str) -> List[LintResult]:
        """Check for consistent formatting."""
        results = []
        lines = content.splitlines()

        # Check for mixed heading styles
        atx_headings = sum(1 for l in lines if re.match(r'^#{1,6}\s', l))
        underline_headings = sum(
            1 for i, l in enumerate(lines)
            if re.match(r'^[=-]+$', l) and i > 0 and lines[i-1].strip()
        )
        if atx_headings > 0 and underline_headings > 0:
            results.append(LintResult(
                rule="consistent-format",
                severity=Severity.INFO,
                message="Mixed heading styles (ATX and underline)",
                suggestion="Use one heading style consistently",
            ))

        # Check for mixed bullet styles
        dash_bullets = sum(1 for l in lines if re.match(r'^\s*-\s', l))
        star_bullets = sum(1 for l in lines if re.match(r'^\s*\*\s', l))
        if dash_bullets > 0 and star_bullets > 0:
            results.append(LintResult(
                rule="consistent-format",
                severity=Severity.INFO,
                message="Mixed bullet styles ('- ' and '* ')",
                suggestion="Use one bullet style consistently (prefer '-')",
            ))

        return results
