"""MCP Code Quality Analyzer.

Analyzes code quality of MCP server implementations beyond security.
Computes cyclomatic complexity, nesting depth, function length, detects
God functions, dead code patterns, and code duplication indicators.
Produces a quality score 0-100.

Zero external dependencies. Python 3.11+ stdlib only.
"""

from __future__ import annotations

import ast
import json
import re
from dataclasses import dataclass, field
from enum import Enum
from typing import Any


# ---------------------------------------------------------------------------
# Enums & Data Models
# ---------------------------------------------------------------------------

class QualityLevel(Enum):
    """Quality rating levels."""
    EXCELLENT = "EXCELLENT"    # 90-100
    GOOD = "GOOD"              # 70-89
    FAIR = "FAIR"              # 50-69
    POOR = "POOR"              # 30-49
    CRITICAL = "CRITICAL"      # 0-29

    @classmethod
    def from_score(cls, score: float) -> QualityLevel:
        if score >= 90:
            return cls.EXCELLENT
        if score >= 70:
            return cls.GOOD
        if score >= 50:
            return cls.FAIR
        if score >= 30:
            return cls.POOR
        return cls.CRITICAL


@dataclass
class FunctionMetrics:
    """Metrics for a single function."""
    name: str
    line: int
    end_line: int = 0
    length: int = 0
    cyclomatic_complexity: int = 1
    nesting_depth: int = 0
    parameter_count: int = 0
    return_count: int = 0
    is_god_function: bool = False
    is_too_long: bool = False
    is_too_complex: bool = False

    @property
    def issues(self) -> list[str]:
        issues = []
        if self.is_god_function:
            issues.append("God function (too many responsibilities)")
        if self.is_too_long:
            issues.append(f"Too long ({self.length} lines, max 100)")
        if self.is_too_complex:
            issues.append(f"Too complex (CC={self.cyclomatic_complexity}, max 10)")
        if self.nesting_depth > 4:
            issues.append(f"Deep nesting (depth={self.nesting_depth}, max 4)")
        if self.parameter_count > 5:
            issues.append(f"Too many params ({self.parameter_count}, max 5)")
        return issues

    def to_dict(self) -> dict[str, Any]:
        return {
            "name": self.name, "line": self.line, "end_line": self.end_line,
            "length": self.length, "cyclomatic_complexity": self.cyclomatic_complexity,
            "nesting_depth": self.nesting_depth, "parameter_count": self.parameter_count,
            "return_count": self.return_count, "is_god_function": self.is_god_function,
            "is_too_long": self.is_too_long, "is_too_complex": self.is_too_complex,
            "issues": self.issues,
        }


@dataclass
class DuplicateBlock:
    """A detected duplicate code block."""
    lines_a: tuple[int, int]   # (start, end) line numbers
    lines_b: tuple[int, int]
    similarity: float = 0.0
    content_hash: str = ""

    def to_dict(self) -> dict[str, Any]:
        return {
            "lines_a": list(self.lines_a), "lines_b": list(self.lines_b),
            "similarity": self.similarity, "content_hash": self.content_hash,
        }


@dataclass
class DeadCodePattern:
    """A detected dead code pattern."""
    line: int
    pattern_type: str  # "unreachable", "unused_import", "commented_code", "unused_variable"
    description: str

    def to_dict(self) -> dict[str, Any]:
        return {"line": self.line, "pattern_type": self.pattern_type,
                "description": self.description}


@dataclass
class QualityIssue:
    """A code quality issue."""
    line: int
    category: str  # "complexity", "length", "naming", "structure", "duplication", "dead_code"
    severity: str  # "HIGH", "MEDIUM", "LOW"
    message: str

    def to_dict(self) -> dict[str, Any]:
        return {"line": self.line, "category": self.category,
                "severity": self.severity, "message": self.message}


@dataclass
class QualityReport:
    """Complete code quality analysis report."""
    total_lines: int = 0
    code_lines: int = 0
    comment_lines: int = 0
    blank_lines: int = 0
    function_count: int = 0
    class_count: int = 0
    avg_function_length: float = 0.0
    avg_complexity: float = 0.0
    max_complexity: int = 0
    max_nesting: int = 0
    quality_score: float = 100.0
    quality_level: QualityLevel = QualityLevel.EXCELLENT
    functions: list[FunctionMetrics] = field(default_factory=list)
    issues: list[QualityIssue] = field(default_factory=list)
    duplicates: list[DuplicateBlock] = field(default_factory=list)
    dead_code: list[DeadCodePattern] = field(default_factory=list)

    @property
    def issue_counts(self) -> dict[str, int]:
        counts: dict[str, int] = {}
        for issue in self.issues:
            counts[issue.severity] = counts.get(issue.severity, 0) + 1
        return counts

    def to_dict(self) -> dict[str, Any]:
        return {
            "total_lines": self.total_lines, "code_lines": self.code_lines,
            "comment_lines": self.comment_lines, "blank_lines": self.blank_lines,
            "function_count": self.function_count, "class_count": self.class_count,
            "avg_function_length": round(self.avg_function_length, 1),
            "avg_complexity": round(self.avg_complexity, 1),
            "max_complexity": self.max_complexity, "max_nesting": self.max_nesting,
            "quality_score": round(self.quality_score, 1),
            "quality_level": self.quality_level.value,
            "issue_counts": self.issue_counts,
            "functions": [f.to_dict() for f in self.functions],
            "issues": [i.to_dict() for i in self.issues],
            "duplicates": [d.to_dict() for d in self.duplicates],
            "dead_code": [d.to_dict() for d in self.dead_code],
        }


# ---------------------------------------------------------------------------
# AST-based analysis helpers
# ---------------------------------------------------------------------------

def _count_branches(node: ast.AST) -> int:
    """Count decision points for cyclomatic complexity."""
    count = 0
    for child in ast.walk(node):
        if isinstance(child, (ast.If, ast.While, ast.For)):
            count += 1
        elif isinstance(child, ast.BoolOp):
            count += len(child.values) - 1
        elif isinstance(child, ast.ExceptHandler):
            count += 1
        elif isinstance(child, ast.Assert):
            count += 1
        elif isinstance(child, (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)):
            count += 1
        elif isinstance(child, ast.IfExp):
            count += 1
    return count


def _max_nesting_depth(node: ast.AST, current: int = 0) -> int:
    """Calculate maximum nesting depth."""
    max_depth = current
    nesting_types = (ast.If, ast.While, ast.For, ast.With, ast.Try)
    for child in ast.iter_child_nodes(node):
        if isinstance(child, nesting_types):
            child_depth = _max_nesting_depth(child, current + 1)
            max_depth = max(max_depth, child_depth)
        else:
            child_depth = _max_nesting_depth(child, current)
            max_depth = max(max_depth, child_depth)
    return max_depth


def _count_returns(node: ast.AST) -> int:
    """Count return statements in a function."""
    count = 0
    for child in ast.walk(node):
        if isinstance(child, ast.Return):
            count += 1
    return count


def _is_god_function(func: ast.FunctionDef | ast.AsyncFunctionDef,
                     length: int, complexity: int) -> bool:
    """Detect God function pattern: too long + too complex + many concerns."""
    if length > 150 and complexity > 15:
        return True
    if length > 100 and complexity > 10:
        # Check for multiple distinct operations
        operations = set()
        for child in ast.walk(func):
            if isinstance(child, ast.Call):
                if isinstance(child.func, ast.Attribute):
                    operations.add(child.func.attr)
                elif isinstance(child.func, ast.Name):
                    operations.add(child.func.id)
        if len(operations) > 15:
            return True
    return False


def _analyze_function(func: ast.FunctionDef | ast.AsyncFunctionDef) -> FunctionMetrics:
    """Analyze a single function."""
    end_line = getattr(func, 'end_lineno', func.lineno) or func.lineno
    length = end_line - func.lineno + 1
    complexity = 1 + _count_branches(func)
    nesting = _max_nesting_depth(func)
    returns = _count_returns(func)

    params = len(func.args.args)
    if func.args.vararg:
        params += 1
    if func.args.kwarg:
        params += 1

    is_god = _is_god_function(func, length, complexity)

    return FunctionMetrics(
        name=func.name,
        line=func.lineno,
        end_line=end_line,
        length=length,
        cyclomatic_complexity=complexity,
        nesting_depth=nesting,
        parameter_count=params,
        return_count=returns,
        is_god_function=is_god,
        is_too_long=length > 100,
        is_too_complex=complexity > 10,
    )


# ---------------------------------------------------------------------------
# Dead code detection
# ---------------------------------------------------------------------------

def _detect_dead_code(code: str, tree: ast.AST) -> list[DeadCodePattern]:
    """Detect dead code patterns."""
    patterns: list[DeadCodePattern] = []
    lines = code.split("\n")

    # Detect commented-out code blocks
    comment_code_re = re.compile(r'^\s*#\s*(def |class |import |from |if |for |while |return )')
    for i, line in enumerate(lines, 1):
        if comment_code_re.match(line):
            patterns.append(DeadCodePattern(
                line=i, pattern_type="commented_code",
                description="Commented-out code detected",
            ))

    # Detect unreachable code after return/raise/break/continue
    for node in ast.walk(tree):
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
            body = node.body
            for i, stmt in enumerate(body):
                if isinstance(stmt, (ast.Return, ast.Raise, ast.Break, ast.Continue)):
                    if i < len(body) - 1:
                        next_stmt = body[i + 1]
                        patterns.append(DeadCodePattern(
                            line=next_stmt.lineno,
                            pattern_type="unreachable",
                            description="Code after return/raise/break is unreachable",
                        ))

    # Detect unused imports (simple heuristic)
    imported_names: dict[str, int] = {}
    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            for alias in node.names:
                name = alias.asname or alias.name.split(".")[0]
                imported_names[name] = node.lineno
        elif isinstance(node, ast.ImportFrom):
            for alias in node.names:
                name = alias.asname or alias.name
                if name != "*":
                    imported_names[name] = node.lineno

    used_names: set[str] = set()
    for node in ast.walk(tree):
        if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load):
            used_names.add(node.id)
        elif isinstance(node, ast.Attribute):
            if isinstance(node.value, ast.Name):
                used_names.add(node.value.id)

    for name, line in imported_names.items():
        if name not in used_names and not name.startswith("_"):
            patterns.append(DeadCodePattern(
                line=line, pattern_type="unused_import",
                description=f"Import '{name}' appears unused",
            ))

    return patterns


# ---------------------------------------------------------------------------
# Duplication detection (simple hash-based)
# ---------------------------------------------------------------------------

def _detect_duplicates(code: str, min_lines: int = 5) -> list[DuplicateBlock]:
    """Detect duplicate code blocks using line hashing."""
    lines = code.split("\n")
    duplicates: list[DuplicateBlock] = []
    seen: dict[str, int] = {}

    for i in range(len(lines) - min_lines + 1):
        block = "\n".join(
            l.strip() for l in lines[i:i + min_lines]
            if l.strip() and not l.strip().startswith("#")
        )
        if len(block) < 40:
            continue

        block_hash = str(hash(block))
        if block_hash in seen:
            prev_start = seen[block_hash]
            # Avoid overlapping blocks
            if abs(prev_start - (i + 1)) >= min_lines:
                duplicates.append(DuplicateBlock(
                    lines_a=(prev_start, prev_start + min_lines - 1),
                    lines_b=(i + 1, i + min_lines),
                    similarity=1.0,
                    content_hash=block_hash[:16],
                ))
        else:
            seen[block_hash] = i + 1

    return duplicates


# ---------------------------------------------------------------------------
# Line counting
# ---------------------------------------------------------------------------

def _count_lines(code: str) -> tuple[int, int, int, int]:
    """Count total, code, comment, and blank lines."""
    total = 0
    code_lines = 0
    comment_lines = 0
    blank_lines = 0
    in_docstring = False
    docstring_char = None

    for line in code.split("\n"):
        total += 1
        stripped = line.strip()

        if not stripped:
            blank_lines += 1
            continue

        if in_docstring:
            comment_lines += 1
            if docstring_char and docstring_char in stripped:
                in_docstring = False
            continue

        if stripped.startswith('"""') or stripped.startswith("'''"):
            char = stripped[:3]
            if stripped.count(char) == 1:
                in_docstring = True
                docstring_char = char
            comment_lines += 1
            continue

        if stripped.startswith("#"):
            comment_lines += 1
            continue

        code_lines += 1

    return total, code_lines, comment_lines, blank_lines


# ---------------------------------------------------------------------------
# Main analysis function
# ---------------------------------------------------------------------------

def analyze_code_quality(code: str) -> QualityReport:
    """Analyze code quality and produce a comprehensive report.

    Args:
        code: Python source code string.

    Returns:
        QualityReport with metrics, issues, and quality score.
    """
    report = QualityReport()

    if not code or not code.strip():
        report.quality_score = 100.0
        report.quality_level = QualityLevel.EXCELLENT
        return report

    # Line counting
    total, code_l, comment_l, blank_l = _count_lines(code)
    report.total_lines = total
    report.code_lines = code_l
    report.comment_lines = comment_l
    report.blank_lines = blank_l

    # Parse AST
    try:
        tree = ast.parse(code)
    except SyntaxError:
        report.quality_score = 0.0
        report.quality_level = QualityLevel.CRITICAL
        report.issues.append(QualityIssue(
            line=1, category="syntax", severity="HIGH",
            message="Code has syntax errors and cannot be parsed",
        ))
        return report

    # Count classes
    for node in ast.walk(tree):
        if isinstance(node, ast.ClassDef):
            report.class_count += 1

    # Analyze functions
    functions: list[FunctionMetrics] = []
    for node in ast.walk(tree):
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
            fm = _analyze_function(node)
            functions.append(fm)

    report.functions = functions
    report.function_count = len(functions)

    if functions:
        lengths = [f.length for f in functions]
        complexities = [f.cyclomatic_complexity for f in functions]
        report.avg_function_length = sum(lengths) / len(lengths)
        report.avg_complexity = sum(complexities) / len(complexities)
        report.max_complexity = max(complexities)
        report.max_nesting = max(f.nesting_depth for f in functions)

    # Generate issues from function metrics
    for fm in functions:
        if fm.is_too_long:
            report.issues.append(QualityIssue(
                line=fm.line, category="length", severity="HIGH",
                message=f"Function '{fm.name}' is {fm.length} lines (max 100)",
            ))
        if fm.is_too_complex:
            report.issues.append(QualityIssue(
                line=fm.line, category="complexity", severity="HIGH",
                message=f"Function '{fm.name}' has CC={fm.cyclomatic_complexity} (max 10)",
            ))
        if fm.nesting_depth > 4:
            report.issues.append(QualityIssue(
                line=fm.line, category="structure", severity="MEDIUM",
                message=f"Function '{fm.name}' has nesting depth {fm.nesting_depth} (max 4)",
            ))
        if fm.parameter_count > 5:
            report.issues.append(QualityIssue(
                line=fm.line, category="structure", severity="MEDIUM",
                message=f"Function '{fm.name}' has {fm.parameter_count} parameters (max 5)",
            ))
        if fm.is_god_function:
            report.issues.append(QualityIssue(
                line=fm.line, category="structure", severity="HIGH",
                message=f"Function '{fm.name}' is a God function — consider splitting",
            ))
        if fm.return_count > 5:
            report.issues.append(QualityIssue(
                line=fm.line, category="structure", severity="LOW",
                message=f"Function '{fm.name}' has {fm.return_count} return statements",
            ))

    # Dead code detection
    report.dead_code = _detect_dead_code(code, tree)
    for dc in report.dead_code:
        sev = "MEDIUM" if dc.pattern_type == "unreachable" else "LOW"
        report.issues.append(QualityIssue(
            line=dc.line, category="dead_code", severity=sev,
            message=dc.description,
        ))

    # Duplication detection
    report.duplicates = _detect_duplicates(code)
    for dup in report.duplicates:
        report.issues.append(QualityIssue(
            line=dup.lines_a[0], category="duplication", severity="MEDIUM",
            message=f"Duplicate code block at lines {dup.lines_a[0]}-{dup.lines_a[1]} "
                    f"and {dup.lines_b[0]}-{dup.lines_b[1]}",
        ))

    # Long lines check
    for i, line in enumerate(code.split("\n"), 1):
        if len(line) > 120:
            report.issues.append(QualityIssue(
                line=i, category="length", severity="LOW",
                message=f"Line is {len(line)} chars (max 120)",
            ))

    # Calculate quality score
    report.quality_score = _calculate_quality_score(report)
    report.quality_level = QualityLevel.from_score(report.quality_score)

    return report


def _calculate_quality_score(report: QualityReport) -> float:
    """Calculate 0-100 quality score from report metrics."""
    score = 100.0

    # Deduct for issues
    for issue in report.issues:
        if issue.severity == "HIGH":
            score -= 5.0
        elif issue.severity == "MEDIUM":
            score -= 2.0
        elif issue.severity == "LOW":
            score -= 0.5

    # Deduct for high average complexity
    if report.avg_complexity > 8:
        score -= 10.0
    elif report.avg_complexity > 5:
        score -= 5.0

    # Deduct for high max complexity
    if report.max_complexity > 15:
        score -= 10.0
    elif report.max_complexity > 10:
        score -= 5.0

    # Deduct for high average function length
    if report.avg_function_length > 80:
        score -= 10.0
    elif report.avg_function_length > 50:
        score -= 5.0

    # Bonus for good comment ratio (10-30% is ideal)
    if report.total_lines > 0:
        comment_ratio = report.comment_lines / report.total_lines
        if 0.1 <= comment_ratio <= 0.3:
            score += 2.0
        elif comment_ratio < 0.05 and report.total_lines > 50:
            score -= 3.0

    # Deduct for duplicates
    score -= len(report.duplicates) * 3.0

    return max(0.0, min(100.0, round(score, 1)))


# ---------------------------------------------------------------------------
# Formatting
# ---------------------------------------------------------------------------

def format_quality_text(report: QualityReport) -> str:
    """Format quality report as human-readable text."""
    lines: list[str] = []
    lines.append("=" * 60)
    lines.append("MCP Code Quality Report")
    lines.append("=" * 60)
    lines.append(f"Quality Score: {report.quality_score:.1f}/100 ({report.quality_level.value})")
    lines.append("")
    lines.append(f"Lines:      {report.total_lines} total, {report.code_lines} code, "
                 f"{report.comment_lines} comments, {report.blank_lines} blank")
    lines.append(f"Functions:  {report.function_count}")
    lines.append(f"Classes:    {report.class_count}")
    lines.append(f"Avg length: {report.avg_function_length:.1f} lines")
    lines.append(f"Avg CC:     {report.avg_complexity:.1f}")
    lines.append(f"Max CC:     {report.max_complexity}")
    lines.append(f"Max nest:   {report.max_nesting}")
    lines.append("")

    counts = report.issue_counts
    if counts:
        lines.append("Issues:")
        for sev in ("HIGH", "MEDIUM", "LOW"):
            c = counts.get(sev, 0)
            if c:
                lines.append(f"  {sev:<8} {c}")
        lines.append("")

    if report.issues:
        lines.append("-" * 60)
        for issue in report.issues[:20]:
            lines.append(f"  L{issue.line:<5} [{issue.severity}] {issue.category}: {issue.message}")
        if len(report.issues) > 20:
            lines.append(f"  ... and {len(report.issues) - 20} more issues")

    return "\n".join(lines)


def format_quality_json(report: QualityReport) -> str:
    """Format quality report as JSON."""
    return json.dumps(report.to_dict(), indent=2, ensure_ascii=False)
