"""
Smart detection and caching for Tier 3 validation decisions.

Tracks build state to determine when full sandbox validation is needed.
"""

import hashlib
import json
import os
from datetime import datetime
from pathlib import Path
from typing import Optional
from dataclasses import dataclass, asdict


@dataclass
class BuildState:
    """Cached state of a successful build."""
    project_name: str
    framework: str
    language: str
    dependency_hash: str
    config_hash: str
    build_plan_hash: str
    status: str  # "success" or "failed"
    timestamp: str
    
    def to_dict(self) -> dict:
        return asdict(self)
    
    @classmethod
    def from_dict(cls, data: dict) -> "BuildState":
        return cls(**data)


class BuildCache:
    """
    Manages build state cache for smart detection.
    
    Stores hashes of key files to detect changes that would
    require re-validation in the sandbox.
    """
    
    CACHE_DIR_NAME = ".xenfra"
    CACHE_FILE_NAME = "build_state.json"
    
    def __init__(self, project_path: Path):
        self.project_path = project_path
        self.cache_dir = project_path / self.CACHE_DIR_NAME
        self.cache_file = self.cache_dir / self.CACHE_FILE_NAME
    
    def get_cached_state(self) -> Optional[BuildState]:
        """
        Get the cached build state if it exists.
        
        Returns:
            BuildState if cache exists, None otherwise
        """
        if not self.cache_file.exists():
            return None
        
        try:
            content = self.cache_file.read_text()
            data = json.loads(content)
            return BuildState.from_dict(data)
        except (json.JSONDecodeError, KeyError, TypeError):
            return None
    
    def save_state(self, state: BuildState):
        """
        Save the build state to cache.
        
        Args:
            state: BuildState to cache
        """
        self.cache_dir.mkdir(exist_ok=True)
        # Add to .gitignore if not already there
        self._ensure_gitignore()
        
        self.cache_file.write_text(json.dumps(state.to_dict(), indent=2))
    
    def _ensure_gitignore(self):
        """Ensure .xenfra is in .gitignore."""
        gitignore = self.project_path / ".gitignore"
        entry = ".xenfra/"
        
        if gitignore.exists():
            content = gitignore.read_text()
            if entry not in content:
                with open(gitignore, "a") as f:
                    f.write(f"\n{entry}\n")
        else:
            gitignore.write_text(f"{entry}\n")
    
    def compute_dependency_hash(self, plan: "BuildPlan") -> str:
        """
        Compute hash of dependency files.
        
        Args:
            plan: BuildPlan with dependency information
            
        Returns:
            Hash string of dependency files
        """
        hasher = hashlib.sha256()
        
        # Main dependency file
        dep_file = self.project_path / plan.dependency_file
        if dep_file.exists():
            hasher.update(dep_file.read_bytes())
        
        # Lock files
        lock_files = {
            "python": ["poetry.lock", "uv.lock", "Pipfile.lock"],
            "node": ["package-lock.json", "yarn.lock", "pnpm-lock.yaml", "bun.lockb"],
            "go": ["go.sum"],
            "rust": ["Cargo.lock"],
            "ruby": ["Gemfile.lock"],
        }
        
        for lock in lock_files.get(plan.language, []):
            lock_path = self.project_path / lock
            if lock_path.exists():
                hasher.update(lock_path.read_bytes())
        
        return hasher.hexdigest()[:16]
    
    def compute_config_hash(self, plan: "BuildPlan") -> str:
        """
        Compute hash of configuration files.
        
        Args:
            plan: BuildPlan with configuration information
            
        Returns:
            Hash string of config files
        """
        hasher = hashlib.sha256()
        
        # xenfra.yaml
        config_file = self.project_path / "xenfra.yaml"
        if config_file.exists():
            hasher.update(config_file.read_bytes())
        
        # Dockerfile
        dockerfile = self.project_path / "Dockerfile"
        if dockerfile.exists():
            hasher.update(dockerfile.read_bytes())
        
        # Docker Compose
        compose_files = ["docker-compose.yml", "docker-compose.yaml"]
        for cf in compose_files:
            cf_path = self.project_path / cf
            if cf_path.exists():
                hasher.update(cf_path.read_bytes())
        
        return hasher.hexdigest()[:16]
    
    def compute_build_plan_hash(self, plan: "BuildPlan") -> str:
        """
        Compute hash of the build plan itself.
        
        Args:
            plan: BuildPlan to hash
            
        Returns:
            Hash string of build plan
        """
        plan_dict = plan.to_dict()
        plan_str = json.dumps(plan_dict, sort_keys=True)
        return hashlib.sha256(plan_str.encode()).hexdigest()[:16]


class SmartDetector:
    """
    Smart detection for Tier 3 (E2B) validation.
    
    Determines when full sandbox validation is needed based on
    changes to dependencies, config, or previous build failures.
    """
    
    def __init__(self, project_path: Path):
        self.project_path = project_path
        self.cache = BuildCache(project_path)
    
    def should_run_sandbox(
        self, 
        plan: "BuildPlan",
        force: bool = False
    ) -> tuple[bool, str]:
        """
        Determine if Tier 3 (E2B) validation should run.
        
        Args:
            plan: BuildPlan for the project
            force: If True, always run sandbox
            
        Returns:
            Tuple of (should_run, reason)
        """
        if force:
            return True, "Forced by --force flag"
        
        # Check for environment variable override
        sandbox_mode = os.environ.get("XENFRA_SANDBOX", "auto").lower()
        
        if sandbox_mode == "force":
            return True, "Forced by XENFRA_SANDBOX=force"
        
        if sandbox_mode == "skip":
            return False, "Skipped by XENFRA_SANDBOX=skip"
        
        # Get cached state
        cached = self.cache.get_cached_state()
        if not cached:
            return True, "First deployment - validation required"
        
        # Compute current hashes
        current_dep_hash = self.cache.compute_dependency_hash(plan)
        current_config_hash = self.cache.compute_config_hash(plan)
        current_plan_hash = self.cache.compute_build_plan_hash(plan)
        
        # Check for failures
        if cached.status == "failed":
            return True, "Previous build failed - re-validation required"
        
        # Check for dependency changes
        if current_dep_hash != cached.dependency_hash:
            return True, "Dependencies changed - validation required"
        
        # Check for config changes
        if current_config_hash != cached.config_hash:
            return True, "Build configuration changed - validation required"
        
        # Check for framework changes
        if plan.framework != cached.framework:
            return True, "Framework changed - validation required"
        
        # Check for build plan changes
        if current_plan_hash != cached.build_plan_hash:
            return True, "Build plan changed - validation required"
        
        return False, "No significant changes - skipping sandbox"
    
    def record_success(self, plan: "BuildPlan"):
        """
        Record a successful build.
        
        Args:
            plan: BuildPlan that succeeded
        """
        state = BuildState(
            project_name=self.project_path.name,
            framework=plan.framework,
            language=plan.language,
            dependency_hash=self.cache.compute_dependency_hash(plan),
            config_hash=self.cache.compute_config_hash(plan),
            build_plan_hash=self.cache.compute_build_plan_hash(plan),
            status="success",
            timestamp=datetime.utcnow().isoformat()
        )
        self.cache.save_state(state)
    
    def record_failure(self, plan: "BuildPlan"):
        """
        Record a failed build.
        
        Args:
            plan: BuildPlan that failed
        """
        state = BuildState(
            project_name=self.project_path.name,
            framework=plan.framework,
            language=plan.language,
            dependency_hash=self.cache.compute_dependency_hash(plan),
            config_hash=self.cache.compute_config_hash(plan),
            build_plan_hash=self.cache.compute_build_plan_hash(plan),
            status="failed",
            timestamp=datetime.utcnow().isoformat()
        )
        self.cache.save_state(state)


def get_detector(project_path: Path) -> SmartDetector:
    """Get smart detector for a project."""
    return SmartDetector(project_path)
