"""
File Checker Module
Main logic for file integrity checking with support for immutable, semi-mutable, and metadata-only files.
"""

import os
import stat
import pwd
import grp
from typing import Dict, List, Optional, Any
from .checksum import ChecksumCalculator
from .section_parser import SectionParser
from .rule_engine import RuleEngine
from .recovery import RecoveryManager
from .logger import IntegrityLogger


class FileChecker:
    """
    Performs comprehensive file integrity checks with three validation tiers:
    1. Immutable: SHA256 verification + permissions + ownership
    2. Semi-mutable: Section-based rules + permissions + ownership
    3. Metadata-only: Existence + size + permissions + ownership
    """
    
    # File type constants
    TYPE_IMMUTABLE = "IMMUTABLE"
    TYPE_SEMI_MUTABLE = "SEMI-MUTABLE"
    TYPE_METADATA_ONLY = "METADATA-ONLY"
    
    def __init__(self, logger: IntegrityLogger, recovery: RecoveryManager,
                 checksum_calc: ChecksumCalculator, hostname: Optional[str] = None,
                 model: Optional[str] = None):
        """
        Initialize the file checker.
        
        Args:
            logger: Logger instance
            recovery: Recovery manager instance
            checksum_calc: Checksum calculator instance
            hostname: System hostname (auto-detected if not provided)
            model: System model (e.g., SM-130, SM-160, SM-170, SM-180)
        """
        self.logger = logger
        self.recovery = recovery
        self.checksum = checksum_calc
        self.model = model or "SM-170"  # Default model
        self.rule_engine = RuleEngine(hostname)
    
    def check_file(self, file_config: Dict[str, Any], manifest_entry: Optional[Dict[str, str]] = None) -> bool:
        """
        Check a single file according to its configuration.
        
        Args:
            file_config: File configuration dictionary
            manifest_entry: SHA256 manifest entry for immutable files
            
        Returns:
            True if file passed all checks or was successfully repaired, False otherwise
        """
        file_path = os.path.expanduser(file_config.get('file_path', ''))
        file_type = self._determine_file_type(file_config, manifest_entry)
        
        if not file_path:
            self.logger.error("", "Missing file_path in configuration")
            return False
        
        self.logger.log_check(file_path, file_type)
        
        exist_ok = True
        # Check if file exists
        if not os.path.exists(file_path):
            exist_ok = self._handle_missing_file(file_path, file_config)
        
        # Check permissions and ownership
        perms_ok = self._check_permissions(file_path, file_config)
        ownership_ok = self._check_ownership(file_path, file_config)
        
        # Type-specific checks
        if file_type == self.TYPE_IMMUTABLE:
            content_ok = self._check_immutable_content(file_path, file_config, manifest_entry)
        elif file_type == self.TYPE_SEMI_MUTABLE:
            content_ok = self._check_semi_mutable_content(file_path, file_config)
        else:  # TYPE_METADATA_ONLY
            content_ok = self._check_metadata_only(file_path)
        
        # Overall result
        all_ok = perms_ok and ownership_ok and content_ok and exist_ok
        
        if all_ok:
            self.logger.log_ok(file_path, "All checks passed")
        
        return all_ok
    
    def _determine_file_type(self, file_config: Dict[str, Any], 
                            manifest_entry: Optional[Dict[str, str]]) -> str:
        """Determine the file type based on configuration."""
        file_type = file_config.get('type', '').upper()
        
        if file_type in [self.TYPE_IMMUTABLE, self.TYPE_SEMI_MUTABLE, self.TYPE_METADATA_ONLY]:
            return file_type
        
        # Legacy support: determine from editable flag
        editable = file_config.get('editable', True)
        has_rules = 'rules' in file_config
        
        if not editable and manifest_entry:
            return self.TYPE_IMMUTABLE
        elif has_rules or file_config.get('type') == 'semi-mutable':
            return self.TYPE_SEMI_MUTABLE
        else:
            return self.TYPE_METADATA_ONLY
    
    def _handle_missing_file(self, file_path: str, file_config: Dict[str, Any]) -> bool:
        """Handle missing file by restoring from reference."""
        self.logger.log_warning(file_path, "File does not exist")
        
        reference_path = file_config.get('reference_path')
        if not reference_path:
            reference_path = f"/usr/share/stellarmate/reference/common{file_path}"
        reference_path = reference_path.replace('{MODEL}', self.model)
        
        if not os.path.exists(reference_path):
            self.logger.log_error(file_path, f"Reference file not found at {reference_path}")
            return False
        
        self.logger.log_repair(file_path, f"Restoring from {reference_path}")
        
        if self.recovery.restore_from_reference(file_path, reference_path, create_backup=False):
            self.logger.info(f"Successfully restored {file_path}")
            return True
        else:
            self.logger.log_error(file_path, "Failed to restore from reference")
            return False
    
    def _check_permissions(self, file_path: str, file_config: Dict[str, Any]) -> bool:
        """Check and fix file permissions."""
        expected_perms = file_config.get('permissions')
        if not expected_perms:
            return True
        
        try:
            expected_mode = int(expected_perms, 8)
            current_stat = os.stat(file_path)
            current_mode = stat.S_IMODE(current_stat.st_mode)
            
            if current_mode != expected_mode:
                self.logger.log_warning(
                    file_path,
                    f"Permission mismatch: {oct(current_mode)} != {oct(expected_mode)}"
                )
                return self._fix_permissions(file_path, file_config)
            
            return True
            
        except ValueError:
            self.logger.log_error(file_path, f"Invalid permission format: {expected_perms}")
            return False
        except Exception as e:
            self.logger.log_error(file_path, f"Error checking permissions: {e}")
            return False
    
    def _fix_permissions(self, file_path: str, file_config: Dict[str, Any]) -> bool:
        """Fix file permissions."""
        expected_perms = file_config.get('permissions')
        if not expected_perms:
            return True
        
        try:
            expected_mode = int(expected_perms, 8)
            os.chmod(file_path, expected_mode)
            self.logger.log_repair(file_path, f"Fixed permissions to {oct(expected_mode)}")
            return True
        except Exception as e:
            self.logger.log_error(file_path, f"Failed to fix permissions: {e}")
            return False
    
    def _check_ownership(self, file_path: str, file_config: Dict[str, Any]) -> bool:
        """Check and fix file ownership."""
        expected_owner = file_config.get('owner')
        if not expected_owner:
            return True
        
        try:
            owner_parts = expected_owner.split(':')
            if len(owner_parts) != 2:
                return True
            
            expected_user, expected_group = owner_parts
            expected_uid = pwd.getpwnam(expected_user).pw_uid
            expected_gid = grp.getgrnam(expected_group).gr_gid
            
            current_stat = os.stat(file_path)
            
            if current_stat.st_uid != expected_uid or current_stat.st_gid != expected_gid:
                current_user = pwd.getpwuid(current_stat.st_uid).pw_name
                current_group = grp.getgrgid(current_stat.st_gid).gr_name
                self.logger.log_warning(
                    file_path,
                    f"Ownership mismatch: {current_user}:{current_group} != {expected_owner}"
                )
                return self._fix_ownership(file_path, file_config)
            
            return True
            
        except KeyError as e:
            self.logger.log_error(file_path, f"Invalid user/group: {e}")
            return False
        except Exception as e:
            self.logger.log_error(file_path, f"Error checking ownership: {e}")
            return False
    
    def _fix_ownership(self, file_path: str, file_config: Dict[str, Any]) -> bool:
        """Fix file ownership."""
        expected_owner = file_config.get('owner')
        if not expected_owner:
            return True
        
        try:
            owner_parts = expected_owner.split(':')
            if len(owner_parts) != 2:
                return True
            
            expected_user, expected_group = owner_parts
            expected_uid = pwd.getpwnam(expected_user).pw_uid
            expected_gid = grp.getgrnam(expected_group).gr_gid
            
            os.chown(file_path, expected_uid, expected_gid)
            self.logger.log_repair(file_path, f"Fixed ownership to {expected_owner}")
            return True
        except Exception as e:
            self.logger.log_error(file_path, f"Failed to fix ownership: {e}")
            return False
    
    def _check_immutable_content(self, file_path: str, file_config: Dict[str, Any],
                                manifest_entry: Optional[Dict[str, str]]) -> bool:
        """Check immutable file content using SHA256 (supports model-specific checksums)."""
        if not manifest_entry:
            self.logger.log_warning(file_path, "No manifest entry available, skipping content check")
            return True
        
        # Check if model-specific
        model_specific = manifest_entry.get('model_specific', False)
        reference_path = manifest_entry.get('reference_path')
        reference_path = reference_path.replace('{MODEL}', self.model)
        
        if model_specific:
            # Model-specific checksums
            checksums = manifest_entry.get('checksums', {})
            model_checksum = checksums.get(self.model)
            
            if not model_checksum:
                self.logger.log_error(file_path, f"No checksum for model {self.model}")
                return False
            
            expected_hash = model_checksum['sha256']
        else:
            # Single checksum for all models
            if 'sha256' not in manifest_entry:
                self.logger.log_warning(file_path, "No SHA256 checksum available, skipping content check")
                return True
            
            expected_hash = manifest_entry['sha256']
        
        # Calculate actual hash
        actual_hash = self.checksum.calculate_sha256(file_path)
        
        if actual_hash is None:
            self.logger.log_error(file_path, "Failed to calculate SHA256")
            return False
        
        if actual_hash != expected_hash:
            self.logger.log_error(
                file_path,
                f"SHA256 mismatch: {actual_hash[:16]}... != {expected_hash[:16]}..."
            )
            return self._repair_immutable_file(file_path, file_config, reference_path)
        
        return True
    
    def _repair_immutable_file(self, file_path: str, file_config: Dict[str, Any],
                              reference_path: Optional[str] = None) -> bool:
        """Repair an immutable file by restoring from reference."""
        if not reference_path:
            reference_path = file_config.get('reference_path')
            if reference_path and '{MODEL}' in reference_path:
                reference_path = reference_path.replace('{MODEL}', self.model)
        
        if not reference_path:
            reference_path = f"/usr/share/stellarmate/reference{file_path}"
        
        if not os.path.exists(reference_path):
            self.logger.log_error(file_path, f"Reference file not found at {reference_path}")
            return False
        
        # Create backup
        backup_path = self.recovery.create_backup(file_path)
        if backup_path:
            self.logger.log_backup(file_path, backup_path)
        
        # Restore from reference
        self.logger.log_repair(file_path, f"Restoring from {reference_path}")
        
        if self.recovery.restore_from_reference(file_path, reference_path, create_backup=False):
            self.logger.info(f"Successfully restored {file_path}")
            return True
        else:
            self.logger.log_error(file_path, "Failed to restore from reference")
            return False
    
    def _check_semi_mutable_content(self, file_path: str, file_config: Dict[str, Any]) -> bool:
        """Check semi-mutable file content using section rules."""
        reference_path = file_config.get('reference_path')
        if not reference_path:
            reference_path = f"/usr/share/stellarmate/reference{file_path}"
        reference_path = reference_path.replace('{MODEL}', self.model)
        
        if not os.path.exists(reference_path):
            self.logger.log_warning(file_path, f"Reference file not found at {reference_path}")
            return True
        
        rules = file_config.get('rules', {})
        section_rules = rules.get('sections', {})
        
        if not section_rules:
            self.logger.log_warning(file_path, "No section rules defined")
            return True
        
        # Validate file
        revert_only_if_empty = file_config.get('revert_only_if_empty', False)
        is_valid, violations, fixes, warnings = self.rule_engine.validate_file(
            file_path, reference_path, section_rules, revert_only_if_empty
        )
        
        # Log warnings (mismatches that are not empty)
        for warning in warnings:
            self.logger.log_warning(file_path, warning)

        if is_valid:
            return True
        
        # Log violations (empty or missing mandatory keys)
        for violation in violations:
            self.logger.log_error(file_path, violation)
        
        # Apply fixes
        if fixes:
            backup_path = self.recovery.create_backup(file_path)
            if backup_path:
                self.logger.log_backup(file_path, backup_path)
            
            self.logger.log_repair(file_path, f"Applying {len(fixes)} section fixes")
            
            if self.rule_engine.apply_fixes(file_path, reference_path, section_rules, fixes):
                self.logger.info(f"Successfully repaired {file_path}")
                return True
            else:
                self.logger.log_error(file_path, "Failed to apply fixes")
                return False
        
        return False
    
    def _check_metadata_only(self, file_path: str) -> bool:
        """Check metadata-only file (existence and size)."""
        try:
            file_stat = os.stat(file_path)
            
            if file_stat.st_size == 0:
                self.logger.log_warning(file_path, "File is empty (0 bytes)")
                return False
            
            return True
            
        except Exception as e:
            self.logger.log_error(file_path, f"Error checking file: {e}")
            return False
