"""Code quality improvement tools for the project."""

import logging
import os
import shlex
import subprocess  # nosec B404 - required for running formatters
import sys
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from pathlib import Path
from typing import List, Optional, Sequence, Tuple

from pylint import lint

logger = logging.getLogger(__name__)

# Whitelist of allowed formatting commands with their exact arguments
ALLOWED_COMMANDS = {
    "autopep8": {
        "command": "autopep8",
        "required_args": ["--in-place", "--aggressive", "--aggressive"],
        "allow_files": True,
    },
    "isort": {
        "command": "isort",
        "required_args": ["--profile", "black"],
        "allow_files": True,
    },
    "black": {
        "command": "black",
        "required_args": ["--quiet"],
        "allow_files": True,
    },
}


@dataclass
class CommandResult:
    """Result of running a command."""

    success: bool
    output: str
    error: Optional[str] = None


class CodeFormatter:
    """Handles code formatting and quality checks."""

    def __init__(self, project_root: Optional[Path] = None):
        """Initialize with project root directory."""
        self.project_root = project_root or Path(__file__).parent.parent
        self.pylintrc = self.project_root / ".pylintrc"

    def is_safe_command(self, command_template: str) -> bool:
        """Validate if command is in the allowed whitelist.

        Security: This method ensures only pre-approved formatting commands are executed.
        Command templates must match exactly with allowed commands and their arguments.
        """
        parts = shlex.split(command_template)
        if not parts:
            return False

        cmd = parts[0]
        if cmd not in ALLOWED_COMMANDS:
            return False

        cmd_config = ALLOWED_COMMANDS[cmd]
        required_args = cmd_config["required_args"]

        # Check if all required arguments are present in order
        arg_pos = 1
        for req_arg in required_args:
            if arg_pos >= len(parts) or parts[arg_pos] != req_arg:
                return False
            arg_pos += 1

        return True

    def get_python_files(self, exclude_prefix: str = "") -> List[str]:
        """Get all Python files in the project except those starting with prefix."""
        python_files = []
        for root, _, files in os.walk(self.project_root):
            # Skip virtual environment and git directories
            if any(excluded in root for excluded in ["venv", ".git", "__pycache__"]):
                continue
            for file in files:
                if file.endswith(".py") and not file.startswith(exclude_prefix):
                    python_files.append(os.path.join(root, file))
        return python_files

    def run_command(self, command: Sequence[str], timeout: int = 30) -> CommandResult:
        """Run a shell command and return the result.

        Args:
            command: List of command arguments (first item is the command)
            timeout: Maximum time to wait for command completion

        Security:
        - Commands are validated against a strict whitelist
        - Arguments are properly quoted
        - shell=False prevents shell injection
        - Only formatting tools are allowed
        """
        if not command:
            return CommandResult(success=False, output="", error="Empty command")

        # Validate command is in whitelist
        cmd_str = " ".join(command)
        if not self.is_safe_command(cmd_str):
            return CommandResult(
                success=False, output="", error=f"Command not in whitelist: {cmd_str}"
            )

        try:
            # nosec B603 - Command is validated against strict whitelist and shell=False is used
            result = subprocess.run(
                command,
                shell=False,  # nosec B603
                check=True,
                capture_output=True,
                text=True,
                timeout=timeout,
            )
            return CommandResult(success=True, output=result.stdout, error=None)
        except subprocess.TimeoutExpired:
            return CommandResult(
                success=False, output="", error=f"Command timed out after {timeout} seconds"
            )
        except subprocess.CalledProcessError as e:
            return CommandResult(success=False, output=e.stdout or "", error=e.stderr)
        except Exception as e:
            return CommandResult(success=False, output="", error=str(e))

    def run_command_batch(
        self, command_template: str, files: List[str], batch_size: int = 10, timeout: int = 30
    ) -> CommandResult:
        """Run a command on batches of files in parallel."""
        if not files:
            return CommandResult(success=True, output="No files to process", error=None)

        # Split command template into base command and args
        cmd_parts = shlex.split(command_template)
        if not cmd_parts:
            return CommandResult(success=False, output="", error="Empty command template")

        cmd_name = cmd_parts[0]
        if cmd_name not in ALLOWED_COMMANDS:
            return CommandResult(
                success=False,
                output="",
                error=f"Command not in whitelist: {cmd_name}",
            )

        all_results = []
        total_batches = (len(files) + batch_size - 1) // batch_size

        with ThreadPoolExecutor() as executor:
            futures = []
            for i in range(0, len(files), batch_size):
                batch = files[i : i + batch_size]
                # Build command: base command + required args + files
                cmd = [cmd_name] + ALLOWED_COMMANDS[cmd_name]["required_args"] + batch
                futures.append(executor.submit(self.run_command, cmd, timeout))
                print(
                    f"\rProcessing batch {(i//batch_size)+1}/{total_batches}...", end="", flush=True
                )

            for future in futures:
                try:
                    result = future.result()
                    if not result.success:
                        return result
                    if result.output:
                        all_results.append(result.output)
                except Exception as e:
                    return CommandResult(success=False, output="", error=str(e))

        print()  # New line after progress
        return CommandResult(success=True, output="\n".join(all_results), error=None)

    def fix_code_style(self) -> bool:
        """Run automatic code fixers."""
        logger.info("Running automatic code fixes...")

        python_files = self.get_python_files("fix_code")
        if not python_files:
            logger.info("No Python files found to process!")
            return True

        logger.info("Found %d Python files to process", len(python_files))
        logger.info("Running autopep8...")

        result = self.run_command_batch("autopep8", python_files, batch_size=5, timeout=60)
        if not result.success:
            logger.error("Error running autopep8: %s", result.error)
            return False

        return True

    def format_code(self) -> bool:
        """Run code formatters."""
        logger.info("Running code formatters...")

        python_files = self.get_python_files("fix_code")

        # Run isort
        logger.info("Running isort...")
        result = self.run_command_batch("isort", python_files)
        if not result.success:
            logger.error("Error running isort: %s", result.error)
            return False

        # Run black
        logger.info("Running black...")
        result = self.run_command_batch("black", python_files)
        if not result.success:
            logger.error("Error running black: %s", result.error)
            return False

        logger.info("Code formatting complete!")
        return True

    def run_pylint(self) -> Tuple[bool, float]:
        """Run pylint and return success status and score."""
        python_files = self.get_python_files("fix_code")
        pylint_args = [f"--rcfile={self.pylintrc}", "--output-format=colorized"] + python_files

        try:
            run = lint.Run(pylint_args, exit=False)
            score = run.linter.stats.global_note
            return score >= 7.0, score
        except Exception as e:
            logger.error("Error running pylint: %s", e)
            return False, 0.0


def main():
    """Run code quality improvement pipeline."""
    logging.basicConfig(level=logging.INFO)
    logger.info("Starting code quality improvement pipeline...")

    formatter = CodeFormatter()

    if not formatter.fix_code_style():
        sys.exit(1)

    if not formatter.format_code():
        sys.exit(1)

    success, score = formatter.run_pylint()
    if success:
        logger.info("Pylint score: %.2f/10", score)
        if score >= 9.0:
            logger.info("Excellent code quality achieved!")
        else:
            logger.info("Acceptable code quality reached.")
    else:
        logger.error("Code quality check failed")
        sys.exit(1)

    logger.info("Code quality pipeline completed successfully!")


if __name__ == "__main__":
    main()
