"""Rule engine for documentation sync issue detection."""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import Literal

import yaml

from scripts.doc_sync.analyze import CodeChange, DocContent


@dataclass
class Rule:
    """A documentation sync rule."""

    name: str
    severity: Literal['error', 'warning', 'info']
    description: str
    description_ko: str
    detect: dict
    action: str
    action_ko: str


@dataclass
class DocSyncIssue:
    """A detected documentation sync issue."""

    rule: str
    severity: Literal['error', 'warning', 'info']
    code_file: str
    doc_file: str
    element_name: str
    description: str
    suggestion: str
    line: int
    suggestion_code: str = ''  # Actual code for GitHub suggestion block


class RuleEngine:
    """Engine for loading and applying documentation sync rules."""

    def __init__(self, rules_path: str) -> None:
        """Initialize rule engine with rules from YAML file.

        Args:
            rules_path: Path to the rules YAML configuration file.
        """
        self.rules_path = Path(rules_path)
        self.rules: dict[str, Rule] = {}
        self.severity_levels: dict[str, dict] = {}
        self._load_rules()

    def _load_rules(self) -> None:
        """Load rules from YAML configuration."""
        with open(self.rules_path) as f:
            config = yaml.safe_load(f)

        self.severity_levels = config.get('severity_levels', {})

        for rule_name, rule_config in config.get('rules', {}).items():
            self.rules[rule_name] = Rule(
                name=rule_name,
                severity=rule_config['severity'],
                description=rule_config.get('description', ''),
                description_ko=rule_config.get('description_ko', ''),
                detect=rule_config.get('detect', {}),
                action=rule_config.get('action', ''),
                action_ko=rule_config.get('action_ko', ''),
            )


def _generate_docstring_template(signature: str, element_type: str) -> str:
    """Generate a Google-style docstring template from signature.

    Args:
        signature: The function/method signature string.
        element_type: Type of element (function, method, class).

    Returns:
        A docstring template string.
    """
    import re

    # Extract function name and parameters from signature
    # Pattern: def func_name(params) -> return_type:
    match = re.match(r'def\s+(\w+)\s*\((.*?)\)\s*(?:->\s*(.+?))?\s*:', signature)
    if not match:
        # For class definitions
        if signature.startswith('class '):
            return '    """Class description.\n\n    Attributes:\n        attr: Description.\n    """'
        return '    """Description."""'

    params_str = match.group(2)
    return_type = match.group(3)

    # Parse parameters
    params = []
    if params_str.strip():
        # Split by comma, handling nested generics
        depth = 0
        current = ''
        for char in params_str:
            if char in '([{':
                depth += 1
            elif char in ')]}':
                depth -= 1
            elif char == ',' and depth == 0:
                if current.strip():
                    params.append(current.strip())
                current = ''
                continue
            current += char
        if current.strip():
            params.append(current.strip())

    # Filter out self/cls
    params = [p for p in params if not p.startswith('self') and not p.startswith('cls')]

    # Build docstring
    lines = ['    """Short description.']

    if params:
        lines.append('')
        lines.append('    Args:')
        for param in params:
            # Extract param name (before : or =)
            param_name = re.split(r'[:=]', param)[0].strip()
            if param_name:
                lines.append(f'        {param_name}: Description.')

    if return_type and return_type.strip() not in ('None', ''):
        lines.append('')
        lines.append('    Returns:')
        lines.append('        Description.')

    lines.append('    """')
    return '\n'.join(lines)


def _generate_suggestion_with_docstring(signature: str, element_type: str) -> str:
    """Generate full suggestion code with signature and docstring.

    Args:
        signature: The function/method signature string.
        element_type: Type of element (function, method, class).

    Returns:
        Complete suggestion code with signature and docstring.
    """
    docstring = _generate_docstring_template(signature, element_type)
    return f'{signature}\n{docstring}'


def detect_issues(
    changes: list[CodeChange],
    doc_contents: dict[str, DocContent],
    engine: RuleEngine,
) -> list[DocSyncIssue]:
    """Detect documentation sync issues based on code changes.

    Args:
        changes: List of code changes to analyze.
        doc_contents: Dictionary of doc file paths to their content.
        engine: Rule engine with loaded rules.

    Returns:
        List of detected documentation sync issues.
    """
    issues: list[DocSyncIssue] = []

    for change in changes:
        # Skip private APIs
        if not change.is_public:
            continue

        # Check each rule against the change
        issues.extend(_check_removed_api_reference(change, doc_contents, engine))
        issues.extend(_check_breaking_signature_change(change, doc_contents, engine))
        issues.extend(_check_missing_docstring(change, engine))
        issues.extend(_check_stale_signature(change, doc_contents, engine))
        issues.extend(_check_missing_new_api_doc(change, doc_contents, engine))

    return issues


def _check_removed_api_reference(
    change: CodeChange,
    doc_contents: dict[str, DocContent],
    engine: RuleEngine,
) -> list[DocSyncIssue]:
    """Check for removed API still referenced in docs."""
    issues = []

    if change.change_type != 'removed':
        return issues

    rule = engine.rules.get('removed_api_reference')
    if not rule:
        return issues

    # Check all docs for references to the removed element
    for doc_path, doc_content in doc_contents.items():
        if change.element_name in doc_content.code_references:
            issues.append(
                DocSyncIssue(
                    rule='removed_api_reference',
                    severity=rule.severity,
                    code_file=change.file_path,
                    doc_file=doc_path,
                    element_name=change.element_name,
                    description=f'삭제된 `{change.element_name}`가 문서에서 여전히 참조되고 있습니다.',
                    suggestion=rule.action_ko,
                    line=change.line_number,
                )
            )

    return issues


def _check_breaking_signature_change(
    change: CodeChange,
    doc_contents: dict[str, DocContent],
    engine: RuleEngine,
) -> list[DocSyncIssue]:
    """Check for breaking signature changes not reflected in docs."""
    issues = []

    if change.change_type != 'modified' or not change.breaking_change:
        return issues

    rule = engine.rules.get('breaking_signature_change')
    if not rule:
        return issues

    # Check docs that reference this element
    for doc_path, doc_content in doc_contents.items():
        if change.element_name in doc_content.code_references:
            issues.append(
                DocSyncIssue(
                    rule='breaking_signature_change',
                    severity=rule.severity,
                    code_file=change.file_path,
                    doc_file=doc_path,
                    element_name=change.element_name,
                    description=f'`{change.element_name}`의 시그니처가 breaking change로 변경되었습니다.',
                    suggestion=rule.action_ko,
                    line=change.line_number,
                )
            )

    return issues


def _check_missing_docstring(
    change: CodeChange,
    engine: RuleEngine,
) -> list[DocSyncIssue]:
    """Check for public API without docstring."""
    issues = []

    if change.change_type == 'removed':
        return issues

    if change.has_docstring:
        return issues

    rule = engine.rules.get('missing_docstring')
    if not rule:
        return issues

    # Generate suggestion code with docstring template
    suggestion_code = ''
    if change.new_signature:
        suggestion_code = _generate_suggestion_with_docstring(change.new_signature, change.element_type)

    issues.append(
        DocSyncIssue(
            rule='missing_docstring',
            severity=rule.severity,
            code_file=change.file_path,
            doc_file=change.file_path,  # Issue is in the code file itself
            element_name=change.element_name,
            description=f'Public API `{change.element_name}`에 docstring이 없습니다.',
            suggestion=rule.action_ko,
            line=change.line_number,
            suggestion_code=suggestion_code,
        )
    )

    return issues


def _check_stale_signature(
    change: CodeChange,
    doc_contents: dict[str, DocContent],
    engine: RuleEngine,
) -> list[DocSyncIssue]:
    """Check for signature changes not updated in docs."""
    issues = []

    if change.change_type != 'modified':
        return issues

    # Check if signature actually changed
    if change.old_signature == change.new_signature:
        return issues

    rule = engine.rules.get('stale_signature')
    if not rule:
        return issues

    # Check docs that reference this element
    for doc_path, doc_content in doc_contents.items():
        if change.element_name in doc_content.code_references:
            # Check if doc has signature lines for this element
            signature_lines = doc_content.signature_lines.get(change.element_name, [])

            if signature_lines:
                # Create issue for each signature line in the doc
                for line_num, old_line in signature_lines:
                    # Generate suggestion: replace old signature with new
                    suggestion_code = change.new_signature if change.new_signature else ''
                    issues.append(
                        DocSyncIssue(
                            rule='stale_signature',
                            severity=rule.severity,
                            code_file=change.file_path,
                            doc_file=doc_path,
                            element_name=change.element_name,
                            description=(
                                f'`{change.element_name}`의 시그니처가 변경되었으나 '
                                f'문서가 업데이트되지 않았습니다.\n\n'
                                f'이전: `{change.old_signature}`\n'
                                f'새로운: `{change.new_signature}`'
                            ),
                            suggestion=rule.action_ko,
                            line=line_num,  # Use doc file line number
                            suggestion_code=suggestion_code,
                        )
                    )
            else:
                # No specific signature line found, create general issue
                issues.append(
                    DocSyncIssue(
                        rule='stale_signature',
                        severity=rule.severity,
                        code_file=change.file_path,
                        doc_file=doc_path,
                        element_name=change.element_name,
                        description=(
                            f'`{change.element_name}`의 시그니처가 변경되었으나 '
                            f'문서가 업데이트되지 않았습니다.\n\n'
                            f'이전: `{change.old_signature}`\n'
                            f'새로운: `{change.new_signature}`'
                        ),
                        suggestion=rule.action_ko,
                        line=0,
                    )
                )

    return issues


def _check_missing_new_api_doc(
    change: CodeChange,
    doc_contents: dict[str, DocContent],
    engine: RuleEngine,
) -> list[DocSyncIssue]:
    """Check for new public API without documentation."""
    issues = []

    if change.change_type != 'added':
        return issues

    if change.element_type not in ('class', 'function'):
        return issues

    rule = engine.rules.get('missing_new_api_doc')
    if not rule:
        return issues

    # Check if the new element is referenced in any doc
    is_documented = False
    for doc_content in doc_contents.values():
        if change.element_name in doc_content.code_references:
            is_documented = True
            break

    if not is_documented:
        issues.append(
            DocSyncIssue(
                rule='missing_new_api_doc',
                severity=rule.severity,
                code_file=change.file_path,
                doc_file='',  # No doc file yet
                element_name=change.element_name,
                description=f'새로운 public API `{change.element_name}`에 대한 문서가 없습니다.',
                suggestion=rule.action_ko,
                line=change.line_number,
            )
        )

    return issues


def filter_by_severity(
    issues: list[DocSyncIssue],
    min_severity: Literal['error', 'warning', 'info'] = 'info',
) -> list[DocSyncIssue]:
    """Filter issues by minimum severity level.

    Args:
        issues: List of issues to filter.
        min_severity: Minimum severity level to include.

    Returns:
        Filtered list of issues.
    """
    severity_order = {'error': 0, 'warning': 1, 'info': 2}
    min_level = severity_order[min_severity]

    return [i for i in issues if severity_order[i.severity] <= min_level]


def count_by_severity(issues: list[DocSyncIssue]) -> dict[str, int]:
    """Count issues by severity level.

    Args:
        issues: List of issues to count.

    Returns:
        Dictionary with counts for each severity level.
    """
    counts = {'error': 0, 'warning': 0, 'info': 0}

    for issue in issues:
        if issue.severity in counts:
            counts[issue.severity] += 1

    return counts


def main() -> None:
    """Main entry point for CLI usage."""
    import argparse
    import json

    parser = argparse.ArgumentParser(description='Apply documentation sync rules to analyzed changes')
    parser.add_argument(
        '--input',
        required=True,
        help='Path to analysis JSON file from analyze.py',
    )
    parser.add_argument(
        '--output',
        '-o',
        help='Output JSON file path for issues',
    )
    parser.add_argument(
        '--rules',
        default='.github/doc-sync-rules.yaml',
        help='Path to rules YAML configuration',
    )

    args = parser.parse_args()

    # Load analysis results
    with open(args.input) as f:
        analysis = json.load(f)

    # Convert to CodeChange objects
    changes = []
    for change_dict in analysis.get('changes', []):
        changes.append(
            CodeChange(
                file_path=change_dict['file_path'],
                change_type=change_dict['change_type'],
                element_type=change_dict['element_type'],
                element_name=change_dict['element_name'],
                old_signature=change_dict.get('old_signature'),
                new_signature=change_dict.get('new_signature'),
                docstring_changed=change_dict.get('docstring_changed', False),
                breaking_change=change_dict.get('breaking_change', False),
                impact_level=change_dict.get('impact_level', 'medium'),
                is_public=change_dict.get('is_public', True),
                has_docstring=change_dict.get('has_docstring', True),
                line_number=change_dict.get('line_number', 0),
            )
        )

    # Build doc_contents from doc_mappings
    doc_contents: dict[str, DocContent] = {}
    doc_mappings = analysis.get('doc_mappings', {})

    for code_file, doc_files in doc_mappings.items():
        for doc_file in doc_files:
            if doc_file not in doc_contents:
                # Read doc file and analyze
                from scripts.doc_sync.analyze import DocumentAnalyzer

                analyzer = DocumentAnalyzer()
                try:
                    content = Path(doc_file).read_text(errors='ignore')
                    doc_contents[doc_file] = analyzer.analyze_document(doc_file, content)
                except Exception:
                    doc_contents[doc_file] = DocContent(
                        file_path=doc_file,
                        code_references=[],
                        code_examples=[],
                        api_signatures=[],
                    )

    # Apply rules
    engine = RuleEngine(args.rules)
    issues = detect_issues(changes, doc_contents, engine)

    # Convert to serializable format
    result = {
        'issues': [
            {
                'rule': issue.rule,
                'severity': issue.severity,
                'code_file': issue.code_file,
                'doc_file': issue.doc_file,
                'element_name': issue.element_name,
                'description': issue.description,
                'suggestion': issue.suggestion,
                'line': issue.line,
                'suggestion_code': issue.suggestion_code,
            }
            for issue in issues
        ],
        'summary': count_by_severity(issues),
    }

    if args.output:
        with open(args.output, 'w') as f:
            json.dump(result, f, indent=2, ensure_ascii=False)
    else:
        print(json.dumps(result, indent=2, ensure_ascii=False))


if __name__ == '__main__':
    main()
