from __future__ import annotations

import json
import logging
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

from .agents import AgentConfig, create_llm_adapter
from .executor import RemediationExecutor

logger = logging.getLogger(__name__)

DEFAULT_STANDARDS_PATH = Path(__file__).parent / "cleanup_standards.md"

SKIP_DIRS = {
    "node_modules",
    ".git",
    "__pycache__",
    ".venv",
    "venv",
    "env",
    ".mypy_cache",
    ".pytest_cache",
    ".tox",
    "dist",
    "build",
    ".eggs",
    "egg-info",
    ".next",
    ".nuxt",
    "coverage",
    ".coverage",
}

CODE_EXTENSIONS = {".py", ".js", ".ts", ".jsx", ".tsx", ".go", ".rs", ".java"}

MAX_FILE_SIZE = 100_000  # bytes
MAX_FILE_LINES = 2000


# -- Structured output schemas --

CLEANUP_ANALYSIS_SCHEMA = {
    "type": "object",
    "additionalProperties": False,
    "required": ["items"],
    "properties": {
        "items": {
            "type": "array",
            "items": {
                "type": "object",
                "additionalProperties": False,
                "required": [
                    "line",
                    "category",
                    "description",
                    "suggestion",
                    "severity",
                ],
                "properties": {
                    "line": {"type": "integer"},
                    "category": {"type": "string"},
                    "description": {"type": "string"},
                    "suggestion": {"type": "string"},
                    "severity": {
                        "type": "string",
                        "enum": ["critical", "high", "medium", "low"],
                    },
                },
            },
        }
    },
}

CLEANUP_ANALYSIS_FORMAT = {
    "type": "json_schema",
    "json_schema": {
        "name": "cleanup_analysis",
        "schema": CLEANUP_ANALYSIS_SCHEMA,
        "strict": True,
    },
}

CLEANUP_FIX_SCHEMA = {
    "type": "object",
    "additionalProperties": False,
    "required": ["code_lines", "confidence", "change_description"],
    "properties": {
        "code_lines": {"type": "array", "items": {"type": "string"}},
        "confidence": {"type": "string", "enum": ["high", "medium", "low"]},
        "change_description": {"type": "string"},
    },
}

CLEANUP_FIX_FORMAT = {
    "type": "json_schema",
    "json_schema": {
        "name": "cleanup_fix",
        "schema": CLEANUP_FIX_SCHEMA,
        "strict": True,
    },
}


# -- Dataclasses --


@dataclass
class CleanupItem:
    file: str
    line: int
    category: str
    description: str
    suggestion: str
    severity: str = "medium"
    status: str = "pending"  # pending | applied | reverted | skipped
    skip_reason: str = ""


@dataclass
class CleanupResult:
    items: list[CleanupItem] = field(default_factory=list)
    applied: int = 0
    reverted: int = 0
    skipped: int = 0
    total_analyzed_files: int = 0

    def summary(self) -> dict:
        return {
            "total_items": len(self.items),
            "applied": self.applied,
            "reverted": self.reverted,
            "skipped": self.skipped,
            "total_analyzed_files": self.total_analyzed_files,
            "items": [
                {
                    "file": it.file,
                    "line": it.line,
                    "category": it.category,
                    "description": it.description,
                    "severity": it.severity,
                    "status": it.status,
                    "skip_reason": it.skip_reason,
                }
                for it in self.items
            ],
        }


def _load_standards(path: str | Path | None) -> str:
    if path:
        p = Path(path)
    else:
        p = DEFAULT_STANDARDS_PATH
    if not p.exists():
        raise FileNotFoundError(f"Standards file not found: {p}")
    return p.read_text(encoding="utf-8")


def _build_analysis_system_prompt(standards_text: str) -> str:
    return f"""\
You are a code quality analyzer. Your job is to review source code against \
the coding standards below and identify violations.

# Coding Standards
{standards_text}

# Instructions
- Review the file and list violations of the above standards.
- For each violation, provide the line number, category, description, and a \
concrete suggestion for how to fix it.
- Assign a severity: critical (security issues, data loss risks), high \
(bugs, logic errors), medium (maintainability, readability), low (style, naming).
- Only report real violations — do not invent issues that don't exist.
- Do not report issues in comments or docstrings.
- If the file has no violations, return an empty items array.

Respond with JSON only.\
"""


def _build_analysis_user_prompt(source: str, file_path: str) -> str:
    numbered = []
    for i, line in enumerate(source.splitlines(), 1):
        numbered.append(f"{i:4d} | {line}")
    code = "\n".join(numbered)
    return f"""\
## File: {file_path}

```
{code}
```

Analyze this file against the coding standards and return violations as JSON.\
"""


def _build_fix_system_prompt(standards_text: str) -> str:
    return f"""\
You are a code fixer. You receive a source file and a specific coding standard \
violation. Your job is to produce the corrected version of the ENTIRE file.

# Coding Standards
{standards_text}

# Instructions
- Fix ONLY the specific violation described. Do not make other changes.
- Return the complete file as an array of lines (one string per line).
- Preserve all existing functionality — the fix must not change behavior.
- Set confidence to "high" if you are certain the fix is correct, "medium" if \
reasonable but not certain, "low" if risky or unsure.
- Provide a brief description of what you changed.

Respond with JSON only.\
"""


def _build_fix_user_prompt(source: str, file_path: str, item: CleanupItem) -> str:
    numbered = []
    for i, line in enumerate(source.splitlines(), 1):
        marker = " >>> " if i == item.line else "     "
        numbered.append(f"{i:4d}{marker}{line}")
    code = "\n".join(numbered)
    return f"""\
## File: {file_path}

```
{code}
```

## Violation to Fix
- Line: {item.line}
- Category: {item.category}
- Description: {item.description}
- Suggestion: {item.suggestion}

Produce the corrected file. Fix ONLY this violation.\
"""


_SEVERITY_ORDER = {"critical": 0, "high": 1, "medium": 2, "low": 3}


class CleanupOrchestrator:
    def __init__(
        self,
        *,
        model: str = "gpt-4.1",
        api_key: str | None = None,
        provider: str | None = None,
        base_url: str | None = None,
        test_cmd: str | None = None,
        standards_path: str | Path | None = None,
    ):
        self.config = AgentConfig(model=model, api_key=api_key)
        if provider:
            self.config.provider = provider
        if base_url:
            self.config.base_url = base_url
        self.test_cmd = test_cmd
        self.standards_text = _load_standards(standards_path)
        self._adapter = None

    def _get_adapter(self):
        if self._adapter is None:
            self._adapter = create_llm_adapter(self.config)
        return self._adapter

    def run(
        self,
        path: str | Path,
        *,
        max_fixes: int = 20,
        dry_run: bool = False,
        quiet: bool = False,
        include_patterns: list[str] | None = None,
        exclude_patterns: list[str] | None = None,
    ) -> dict:
        from rich.console import Console

        console = Console(stderr=True)
        log = console.print if not quiet else (lambda *a, **kw: None)

        result = CleanupResult()
        target = Path(path).resolve()

        files = self._collect_files(target, include_patterns, exclude_patterns)
        log(f"[bold]Phase 1:[/bold] Collected {len(files)} files to analyze")

        if not files:
            log("[yellow]No files to analyze.[/yellow]")
            return result.summary()

        log("[bold]Phase 2:[/bold] Analyzing files for standards violations...")
        all_items: list[CleanupItem] = []
        for fp in files:
            try:
                items = self._analyze_file(fp, log)
                all_items.extend(items)
                result.total_analyzed_files += 1
            except Exception as e:
                log(f"[red]Error analyzing {fp}: {e}[/red]")

        log(
            f"  Found {len(all_items)} violations across {result.total_analyzed_files} files"
        )

        if not all_items:
            log("[green]No violations found.[/green]")
            return result.summary()

        all_items.sort(key=lambda it: _SEVERITY_ORDER.get(it.severity, 99))
        if len(all_items) > max_fixes:
            for it in all_items[max_fixes:]:
                it.status = "skipped"
                it.skip_reason = "exceeded max_fixes limit"
                result.skipped += 1
            all_items = all_items[:max_fixes]

        result.items = all_items + [it for it in result.items if it.status == "skipped"]

        if dry_run:
            log(
                f"\n[bold]Dry run:[/bold] {len(all_items)} violations found (no changes applied)"
            )
            for it in all_items:
                log(
                    f"  [{it.severity}] {it.file}:{it.line} "
                    f"[{it.category}] {it.description}"
                )
                it.status = "skipped"
                it.skip_reason = "dry run"
                result.skipped += 1
            result.items = all_items
            return result.summary()

        log(f"\n[bold]Phase 4:[/bold] Applying {len(all_items)} fixes...")
        if target.is_dir():
            project_root = target
        else:
            project_root = target.parent
        executor = RemediationExecutor(
            test_cmd=self.test_cmd, project_root=project_root
        )

        for idx, item in enumerate(all_items, 1):
            log(
                f"\n  [{idx}/{len(all_items)}] {item.file}:{item.line} "
                f"[{item.category}] {item.description}"
            )
            try:
                self._apply_single_fix(item, executor, log)
                if item.status == "applied":
                    result.applied += 1
                elif item.status == "reverted":
                    result.reverted += 1
                elif item.status == "skipped":
                    result.skipped += 1
            except Exception as e:
                item.status = "skipped"
                item.skip_reason = f"error: {e}"
                result.skipped += 1
                log(f"    [red]Error: {e}[/red]")

        result.items = all_items

        log(
            f"\n[bold]Done:[/bold] {result.applied} applied, "
            f"{result.reverted} reverted, {result.skipped} skipped"
        )
        return result.summary()

    def _collect_files(
        self,
        path: Path,
        include_patterns: list[str] | None = None,
        exclude_patterns: list[str] | None = None,
    ) -> list[Path]:
        if path.is_file():
            if path.suffix in CODE_EXTENSIONS:
                return [path]
            return []

        files = []
        for fp in sorted(path.rglob("*")):
            if not fp.is_file():
                continue
            if fp.suffix not in CODE_EXTENSIONS:
                continue
            parts = set(fp.relative_to(path).parts)
            if parts & SKIP_DIRS:
                continue
            if any(p.endswith(".egg-info") for p in fp.relative_to(path).parts):
                continue
            try:
                if fp.stat().st_size > MAX_FILE_SIZE:
                    continue
            except OSError:
                continue
            files.append(fp)

        return files

    def _analyze_file(self, file_path: Path, log) -> list[CleanupItem]:
        source = file_path.read_text(encoding="utf-8", errors="replace")
        lines = source.splitlines()
        if len(lines) > MAX_FILE_LINES:
            log(
                f"  [dim]Skipping {file_path} ({len(lines)} lines > {MAX_FILE_LINES})[/dim]"
            )
            return []

        system = _build_analysis_system_prompt(self.standards_text)
        user = _build_analysis_user_prompt(source, str(file_path))

        try:
            response = self._get_adapter().complete(
                system, user, response_format=CLEANUP_ANALYSIS_FORMAT
            )
        except Exception as e:
            logger.warning("LLM analysis call failed for %s: %s", file_path, e)
            raise

        try:
            data = json.loads(response)
        except json.JSONDecodeError:
            logger.warning("Failed to parse LLM response for %s", file_path)
            return []

        items = []
        for raw in data.get("items", []):
            items.append(
                CleanupItem(
                    file=str(file_path),
                    line=raw.get("line", 0),
                    category=raw.get("category", "unknown"),
                    description=raw.get("description", ""),
                    suggestion=raw.get("suggestion", ""),
                    severity=raw.get("severity", "medium"),
                )
            )

        return items

    def _apply_single_fix(
        self, item: CleanupItem, executor: RemediationExecutor, log
    ) -> None:
        fp = Path(item.file)
        if not fp.exists():
            item.status = "skipped"
            item.skip_reason = "file not found"
            return

        source = fp.read_text(encoding="utf-8", errors="replace")

        system = _build_fix_system_prompt(self.standards_text)
        user = _build_fix_user_prompt(source, str(fp), item)

        try:
            response = self._get_adapter().complete(
                system, user, response_format=CLEANUP_FIX_FORMAT
            )
        except Exception as e:
            item.status = "skipped"
            item.skip_reason = f"LLM fix call failed: {e}"
            return

        try:
            data = json.loads(response)
        except json.JSONDecodeError:
            item.status = "skipped"
            item.skip_reason = "failed to parse fix response"
            return

        confidence = data.get("confidence", "medium")
        if confidence == "low":
            item.status = "skipped"
            item.skip_reason = "low confidence"
            log("    [dim]Skipped (low confidence)[/dim]")
            return

        code_lines = data.get("code_lines", [])
        if not code_lines:
            item.status = "skipped"
            item.skip_reason = "empty fix"
            return

        fixed_code = "\n".join(str(line) for line in code_lines) + "\n"

        if fixed_code == source or fixed_code.rstrip() == source.rstrip():
            item.status = "skipped"
            item.skip_reason = "no change"
            log("    [dim]Skipped (no change)[/dim]")
            return

        if not executor.apply_fix(str(fp), fixed_code):
            item.status = "skipped"
            item.skip_reason = "failed to write file"
            return

        test_result = executor.run_tests()
        if test_result.passed:
            item.status = "applied"
            desc = data.get("change_description", "")
            log(f"    [green]Applied[/green] — {desc}")
        else:
            executor.revert_fix(str(fp))
            item.status = "reverted"
            item.skip_reason = f"tests failed: {test_result.output[:200]}"
            log(f"    [red]Reverted[/red] — tests failed")
