#!/usr/bin/env python3
"""Generate baseline.json from current violations.

Run this script to create or update the baseline file with all
current violations. This should be done:
1. After initial migration to baseline enforcement
2. After fixing violations to update the baseline

Usage:
    python -m tests.quality.generate_baseline
"""

from __future__ import annotations

import ast
import re
import sys
from pathlib import Path

# Add project root to path
sys.path.insert(0, str(Path(__file__).parent.parent.parent))

from tests.quality._baseline import (
    Violation,
    content_hash,
    save_baseline,
)
from tests.quality._helpers import (
    find_source_files,
    parse_file_safe,
    read_file_safe,
    relative_path,
)
from tests.quality._test_smell_collectors import (
    collect_assertion_roulette,
    collect_conditional_test_logic,
    collect_duplicate_test_names,
    collect_eager_tests,
    collect_exception_handling,
    collect_fixture_missing_type,
    collect_fixture_scope_too_narrow,
    collect_long_tests,
    collect_magic_number_tests,
    collect_raises_without_match,
    collect_redundant_prints,
    collect_sensitive_equality,
    collect_sleepy_tests,
    collect_unknown_tests,
    collect_unused_fixtures,
)


def collect_stale_todos() -> list[Violation]:
    """Collect stale TODO violations."""
    stale_pattern = re.compile(
        r"#\s*(TODO|FIXME|HACK|XXX|DEPRECATED)[\s:]*(.{0,100})",
        re.IGNORECASE,
    )
    violations: list[Violation] = []

    for py_file in find_source_files():
        content, error = read_file_safe(py_file)
        if error or content is None:
            continue

        rel_path = relative_path(py_file)
        lines = content.splitlines()

        for i, line in enumerate(lines, start=1):
            if match := stale_pattern.search(line):
                tag = match[1].upper()
                message = match[2].strip()[:50]
                violations.append(
                    Violation(
                        rule="stale_todo",
                        relative_path=rel_path,
                        identifier=content_hash(f"{i}:{line.strip()}"),
                        detail=f"{tag}:{message}",
                    )
                )

    return violations


def collect_orphaned_imports() -> list[Violation]:
    """Collect orphaned import violations."""
    violations: list[Violation] = []

    for py_file in find_source_files(include_init=False):
        tree, error = parse_file_safe(py_file)
        if error or tree is None:
            continue

        rel_path = relative_path(py_file)

        imported_names: set[str] = set()
        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.add(name)
            elif isinstance(node, ast.ImportFrom):
                for alias in node.names:
                    if alias.name != "*":
                        name = alias.asname or alias.name
                        imported_names.add(name)

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

        # Check for __all__ re-exports
        all_exports: set[str] = set()
        for node in ast.walk(tree):
            if isinstance(node, ast.Assign):
                for target in node.targets:
                    if isinstance(target, ast.Name) and target.id == "__all__":
                        if isinstance(node.value, ast.List):
                            for elt in node.value.elts:
                                if isinstance(elt, ast.Constant) and isinstance(
                                    elt.value, str
                                ):
                                    all_exports.add(elt.value)

        # Check for TYPE_CHECKING imports
        type_checking_imports: set[str] = set()
        for node in ast.walk(tree):
            if isinstance(node, ast.If):
                if isinstance(node.test, ast.Name) and node.test.id == "TYPE_CHECKING":
                    for subnode in ast.walk(node):
                        if isinstance(subnode, ast.ImportFrom):
                            for alias in subnode.names:
                                name = alias.asname or alias.name
                                type_checking_imports.add(name)

        unused = imported_names - used_names - type_checking_imports - all_exports
        unused -= {"__future__", "annotations"}

        violations.extend(
            Violation(
                rule="orphaned_import",
                relative_path=rel_path,
                identifier=name,
            )
            for name in sorted(unused)
            if not name.startswith("_")
        )
    return violations


def collect_deprecated_patterns() -> list[Violation]:
    """Collect deprecated pattern violations."""
    deprecated_patterns = [
        (r"\.format\s*\(", "str.format()"),
        (r"% \(", "%-formatting"),
        (r"from typing import Optional", "Optional[X]"),
        (r"from typing import Union", "Union[X, Y]"),
        (r"from typing import List\b", "List[X]"),
        (r"from typing import Dict\b", "Dict[K, V]"),
        (r"from typing import Tuple\b", "Tuple[X, ...]"),
        (r"from typing import Set\b", "Set[X]"),
    ]

    violations: list[Violation] = []

    for py_file in find_source_files():
        content, error = read_file_safe(py_file)
        if error or content is None:
            continue

        rel_path = relative_path(py_file)
        lines = content.splitlines()

        for i, line in enumerate(lines, start=1):
            violations.extend(
                Violation(
                    rule="deprecated_pattern",
                    relative_path=rel_path,
                    identifier=content_hash(f"{i}:{line.strip()}"),
                    detail=old_style,
                )
                for pattern, old_style in deprecated_patterns
                if re.search(pattern, line)
            )
    return violations


def collect_high_complexity() -> list[Violation]:
    """Collect high complexity violations."""
    max_complexity = 15
    violations: list[Violation] = []

    def count_branches(node: ast.AST) -> int:
        count = 0
        for child in ast.walk(node):
            if isinstance(
                child, (ast.If, ast.While, ast.For, ast.AsyncFor, ast.And, ast.Or)
            ):
                count += 1
            elif isinstance(child, ast.comprehension):
                count += 1
                count += len(child.ifs)
            elif isinstance(child, ast.ExceptHandler):
                count += 1
        return count

    for py_file in find_source_files(include_migrations=False):
        tree, error = parse_file_safe(py_file)
        if error or tree is None:
            continue

        rel_path = relative_path(py_file)

        for node in ast.walk(tree):
            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                complexity = count_branches(node) + 1
                if complexity > max_complexity:
                    violations.append(
                        Violation(
                            rule="high_complexity",
                            relative_path=rel_path,
                            identifier=node.name,
                            detail=f"complexity={complexity}",
                        )
                    )

    return violations


def collect_long_parameter_lists() -> list[Violation]:
    """Collect long parameter list violations."""
    max_params = 7
    violations: list[Violation] = []

    for py_file in find_source_files(include_migrations=False):
        tree, error = parse_file_safe(py_file)
        if error or tree is None:
            continue

        rel_path = relative_path(py_file)

        for node in ast.walk(tree):
            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                args = node.args
                total_params = (
                    len(args.args) + len(args.posonlyargs) + len(args.kwonlyargs)
                )
                if "self" in [a.arg for a in args.args]:
                    total_params -= 1
                if "cls" in [a.arg for a in args.args]:
                    total_params -= 1

                if total_params > max_params:
                    violations.append(
                        Violation(
                            rule="long_parameter_list",
                            relative_path=rel_path,
                            identifier=node.name,
                            detail=f"params={total_params}",
                        )
                    )

    return violations


def collect_thin_wrappers() -> list[Violation]:
    """Collect thin wrapper violations."""
    allowed_wrappers = {
        ("get_settings", "_load_settings"),
        ("get_trigger_settings", "_load_trigger_settings"),
        ("get_feature_flags", "model_validate"),
        ("get_calendar_settings", "model_validate"),
        ("from_args", "cls"),
        ("segment_count", "len"),
        ("full_transcript", "join"),
        ("duration", "sub"),
        ("is_active", "property"),
        ("is_admin", "can_admin"),
        ("get_metadata", "get"),
        ("evaluate", "RuleResult"),
        ("database_url_str", "str"),
        ("generate_request_id", "str"),
        ("get_request_id", "get"),
        ("get_user_id", "get"),
        ("get_workspace_id", "get"),
        ("datetime_to_epoch_seconds", "timestamp"),
        ("datetime_to_iso_string", "isoformat"),
        ("epoch_seconds_to_datetime", "fromtimestamp"),
        ("proto_timestamp_to_datetime", "replace"),
    }

    def is_thin_wrapper(node: ast.FunctionDef | ast.AsyncFunctionDef) -> str | None:
        body_stmts = [
            s
            for s in node.body
            if not isinstance(s, ast.Expr) or not isinstance(s.value, ast.Constant)
        ]
        if len(body_stmts) == 1:
            stmt = body_stmts[0]
            if isinstance(stmt, ast.Return) and isinstance(stmt.value, ast.Call):
                call = stmt.value
                if isinstance(call.func, ast.Name):
                    return call.func.id
                elif isinstance(call.func, ast.Attribute):
                    return call.func.attr
        return None

    violations: list[Violation] = []

    for py_file in find_source_files():
        tree, error = parse_file_safe(py_file)
        if error or tree is None:
            continue

        rel_path = relative_path(py_file)

        for node in ast.walk(tree):
            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                if node.name.startswith("_"):
                    continue

                wrapped = is_thin_wrapper(node)
                if wrapped and node.name != wrapped:
                    if (node.name, wrapped) in allowed_wrappers:
                        continue
                    violations.append(
                        Violation(
                            rule="thin_wrapper",
                            relative_path=rel_path,
                            identifier=node.name,
                            detail=wrapped,
                        )
                    )

    return violations


def collect_long_methods() -> list[Violation]:
    """Collect long method violations."""
    max_lines = 75
    violations: list[Violation] = []

    def count_function_lines(node: ast.FunctionDef | ast.AsyncFunctionDef) -> int:
        return 0 if node.end_lineno is None else node.end_lineno - node.lineno + 1

    for py_file in find_source_files(include_migrations=False):
        tree, error = parse_file_safe(py_file)
        if error or tree is None:
            continue

        rel_path = relative_path(py_file)

        for node in ast.walk(tree):
            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                lines = count_function_lines(node)
                if lines > max_lines:
                    violations.append(
                        Violation(
                            rule="long_method",
                            relative_path=rel_path,
                            identifier=node.name,
                            detail=f"lines={lines}",
                        )
                    )

    return violations


def collect_module_size_soft() -> list[Violation]:
    """Collect module size soft limit violations."""
    soft_limit = 500
    violations: list[Violation] = []

    for py_file in find_source_files(include_migrations=False):
        content, error = read_file_safe(py_file)
        if error or content is None:
            continue

        rel_path = relative_path(py_file)
        line_count = len(content.splitlines())

        if line_count > soft_limit:
            violations.append(
                Violation(
                    rule="module_size_soft",
                    relative_path=rel_path,
                    identifier="module",
                    detail=f"lines={line_count}",
                )
            )

    return violations


def collect_alias_imports() -> list[Violation]:
    """Collect alias import violations."""
    alias_pattern = re.compile(r"^import\s+(\w+)\s+as\s+(\w+)")
    from_alias_pattern = re.compile(r"from\s+\S+\s+import\s+(\w+)\s+as\s+(\w+)")

    violations: list[Violation] = []

    for py_file in find_source_files():
        content, error = read_file_safe(py_file)
        if error or content is None:
            continue

        rel_path = relative_path(py_file)
        lines = content.splitlines()

        for i, line in enumerate(lines, start=1):
            for pattern in [alias_pattern, from_alias_pattern]:
                if match := pattern.search(line):
                    original, alias = match.groups()
                    if original.lower() not in alias.lower():
                        if alias not in {"np", "pd", "plt", "tf", "nn", "F", "sa", "sd"}:
                            violations.append(
                                Violation(
                                    rule="alias_import",
                                    relative_path=rel_path,
                                    identifier=content_hash(f"{i}:{line.strip()}"),
                                    detail=f"{original}->{alias}",
                                )
                            )

    return violations


def collect_god_classes() -> list[Violation]:
    """Collect god class violations."""
    max_methods = 20
    max_lines = 500
    violations: list[Violation] = []

    for py_file in find_source_files(include_migrations=False):
        tree, error = parse_file_safe(py_file)
        if error or tree is None:
            continue

        rel_path = relative_path(py_file)

        for node in ast.walk(tree):
            if isinstance(node, ast.ClassDef):
                methods = [
                    n
                    for n in node.body
                    if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
                ]

                if len(methods) > max_methods:
                    violations.append(
                        Violation(
                            rule="god_class",
                            relative_path=rel_path,
                            identifier=node.name,
                            detail=f"methods={len(methods)}",
                        )
                    )

                if node.end_lineno:
                    class_lines = node.end_lineno - node.lineno + 1
                    if class_lines > max_lines:
                        violations.append(
                            Violation(
                                rule="god_class",
                                relative_path=rel_path,
                                identifier=node.name,
                                detail=f"lines={class_lines}",
                            )
                        )

    return violations


def collect_deep_nesting() -> list[Violation]:
    """Collect deep nesting violations."""
    max_nesting = 5
    violations: list[Violation] = []

    def count_nesting_depth(node: ast.AST, current_depth: int = 0) -> int:
        max_depth = current_depth
        nesting_nodes = (
            ast.If, ast.While, ast.For, ast.AsyncFor,
            ast.With, ast.AsyncWith, ast.Try,
            ast.FunctionDef, ast.AsyncFunctionDef,
        )
        for child in ast.iter_child_nodes(node):
            if isinstance(child, nesting_nodes):
                child_depth = count_nesting_depth(child, current_depth + 1)
            else:
                child_depth = count_nesting_depth(child, current_depth)
            max_depth = max(max_depth, child_depth)
        return max_depth

    for py_file in find_source_files(include_migrations=False):
        tree, error = parse_file_safe(py_file)
        if error or tree is None:
            continue

        rel_path = relative_path(py_file)

        for node in ast.walk(tree):
            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                depth = count_nesting_depth(node)
                if depth > max_nesting:
                    violations.append(
                        Violation(
                            rule="deep_nesting",
                            relative_path=rel_path,
                            identifier=node.name,
                            detail=f"depth={depth}",
                        )
                    )

    return violations


def collect_feature_envy() -> list[Violation]:
    """Collect feature envy violations."""
    excluded_class_patterns = {"converter", "exporter", "repository", "repo"}
    excluded_method_patterns = {"_to_domain", "_to_proto", "_proto_to_", "_to_orm", "_from_orm", "export", "_parse"}
    excluded_object_names = {
        "model", "meeting", "segment", "request", "response", "np", "noteflow_pb2",
        "seg", "job", "repo", "ai", "summary", "MeetingState", "event", "item",
        "entity", "uow", "span", "host", "logger", "data", "config",
    }

    violations: list[Violation] = []

    for py_file in find_source_files(include_migrations=False):
        tree, error = parse_file_safe(py_file)
        if error or tree is None:
            continue

        rel_path = relative_path(py_file)

        for class_node in ast.walk(tree):
            if not isinstance(class_node, ast.ClassDef):
                continue

            if any(p in class_node.name.lower() for p in excluded_class_patterns):
                continue

            for method in class_node.body:
                if not isinstance(method, (ast.FunctionDef, ast.AsyncFunctionDef)):
                    continue

                if any(p in method.name.lower() for p in excluded_method_patterns):
                    continue

                self_accesses = 0
                other_accesses: dict[str, int] = {}

                for node in ast.walk(method):
                    if isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name):
                        if node.value.id == "self":
                            self_accesses += 1
                        else:
                            other_accesses[node.value.id] = other_accesses.get(node.value.id, 0) + 1

                for other_obj, count in other_accesses.items():
                    if other_obj in excluded_object_names:
                        continue
                    if count > self_accesses + 3 and count > 5:
                        violations.append(
                            Violation(
                                rule="feature_envy",
                                relative_path=rel_path,
                                identifier=f"{class_node.name}.{method.name}",
                                detail=f"{other_obj}={count}_vs_self={self_accesses}",
                            )
                        )

    return violations


def collect_redundant_type_aliases() -> list[Violation]:
    """Collect redundant type alias violations."""
    violations: list[Violation] = []

    for py_file in find_source_files():
        tree, error = parse_file_safe(py_file)
        if error or tree is None:
            continue

        rel_path = relative_path(py_file)

        for node in ast.walk(tree):
            if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
                if isinstance(node.annotation, ast.Name):
                    if node.annotation.id == "TypeAlias":
                        if isinstance(node.value, ast.Name):
                            base_type = node.value.id
                            if base_type in {"str", "int", "float", "bool", "bytes"}:
                                target_name = node.target.id
                                violations.append(
                                    Violation(
                                        rule="redundant_type_alias",
                                        relative_path=rel_path,
                                        identifier=target_name,
                                        detail=base_type,
                                    )
                                )

    return violations


def collect_passthrough_classes() -> list[Violation]:
    """Collect passthrough class violations."""
    violations: list[Violation] = []

    def is_thin_wrapper(node: ast.FunctionDef | ast.AsyncFunctionDef) -> str | None:
        body_stmts = [
            s for s in node.body
            if not isinstance(s, ast.Expr) or not isinstance(s.value, ast.Constant)
        ]
        if len(body_stmts) == 1:
            stmt = body_stmts[0]
            if isinstance(stmt, ast.Return) and isinstance(stmt.value, ast.Call):
                call = stmt.value
                if isinstance(call.func, ast.Name):
                    return call.func.id
                elif isinstance(call.func, ast.Attribute):
                    return call.func.attr
        return None

    for py_file in find_source_files():
        tree, error = parse_file_safe(py_file)
        if error or tree is None:
            continue

        rel_path = relative_path(py_file)

        for node in ast.walk(tree):
            if isinstance(node, ast.ClassDef):
                methods = [
                    n for n in node.body
                    if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
                    and not n.name.startswith("_")
                ]

                if len(methods) >= 3:
                    passthrough_count = sum(
                        1 for m in methods if is_thin_wrapper(m) is not None
                    )

                    if passthrough_count == len(methods):
                        violations.append(
                            Violation(
                                rule="passthrough_class",
                                relative_path=rel_path,
                                identifier=node.name,
                                detail=f"{passthrough_count}_methods",
                            )
                        )

    return violations


def main() -> None:
    """Generate baseline from all current violations."""
    print("Collecting violations...")

    all_violations: dict[str, list[Violation]] = {}

    # Collect from all rules - source code smells
    collectors = [
        ("stale_todo", collect_stale_todos),
        ("orphaned_import", collect_orphaned_imports),
        ("deprecated_pattern", collect_deprecated_patterns),
        ("high_complexity", collect_high_complexity),
        ("long_parameter_list", collect_long_parameter_lists),
        ("thin_wrapper", collect_thin_wrappers),
        ("long_method", collect_long_methods),
        ("module_size_soft", collect_module_size_soft),
        ("alias_import", collect_alias_imports),
        ("god_class", collect_god_classes),
        ("deep_nesting", collect_deep_nesting),
        ("feature_envy", collect_feature_envy),
        ("redundant_type_alias", collect_redundant_type_aliases),
        ("passthrough_class", collect_passthrough_classes),
        # Test smell collectors
        ("assertion_roulette", collect_assertion_roulette),
        ("conditional_test_logic", collect_conditional_test_logic),
        ("sleepy_test", collect_sleepy_tests),
        ("unknown_test", collect_unknown_tests),
        ("redundant_print", collect_redundant_prints),
        ("exception_handling", collect_exception_handling),
        ("magic_number_test", collect_magic_number_tests),
        ("sensitive_equality", collect_sensitive_equality),
        ("eager_test", collect_eager_tests),
        ("duplicate_test_name", collect_duplicate_test_names),
        ("long_test", collect_long_tests),
        ("fixture_missing_type", collect_fixture_missing_type),
        ("unused_fixture", collect_unused_fixtures),
        ("fixture_scope_too_narrow", collect_fixture_scope_too_narrow),
        ("raises_without_match", collect_raises_without_match),
    ]

    for rule_name, collector in collectors:
        if violations := collector():
            all_violations[rule_name] = violations
            print(f"  {rule_name}: {len(violations)} violations")

    # Save baseline
    save_baseline(all_violations)

    total = sum(len(v) for v in all_violations.values())
    print(f"\nBaseline generated with {total} total violations across {len(all_violations)} rules")
    print(f"Saved to: {Path(__file__).parent / 'baselines.json'}")


if __name__ == "__main__":
    main()
