"""
Fanfic Writer v2.0 - Configuration Manager
Handles loading, validation, and updates of 0-book-config.json
"""
import os
import json
from pathlib import Path
from typing import Dict, Any, Optional, List, Tuple
from datetime import datetime

from .atomic_io import atomic_write_json, atomic_append_jsonl
from .utils import get_timestamp_iso


# ============================================================================
# Configuration Schema
# ============================================================================

CONFIG_SCHEMA = {
    'version': {'type': str, 'required': True, 'default': '2.0.0'},
    'book': {
        'type': dict,
        'required': True,
        'fields': {
            'title': {'type': str, 'required': True},
            'title_slug': {'type': str, 'required': True},
            'book_uid': {'type': str, 'required': True},
            'subtitle': {'type': (str, type(None)), 'required': False, 'default': None},
            'genre': {'type': str, 'required': True},
            'subgenre': {'type': (str, type(None)), 'required': False, 'default': None},
            'target_word_count': {'type': int, 'required': True, 'min': 50000, 'max': 500000},
            'chapter_target_words': {'type': int, 'required': False, 'default': 2500, 'min': 1500, 'max': 8000},
            'language': {'type': str, 'required': False, 'default': 'zh'},
            'rating': {'type': str, 'required': False, 'default': 'PG-13'},
            'tone': {'type': str, 'required': False, 'default': '轻松'}
        }
    },
    'generation': {
        'type': dict,
        'required': True,
        'fields': {
            'model': {'type': str, 'required': True, 'default': 'nvidia/moonshotai/kimi-k2.5'},
            'temperature_outline': {'type': float, 'required': False, 'default': 0.8, 'min': 0.0, 'max': 1.0},
            'temperature_chapter': {'type': float, 'required': False, 'default': 0.75, 'min': 0.0, 'max': 1.0},
            'max_attempts': {'type': int, 'required': False, 'default': 3, 'min': 1, 'max': 5},
            'mode': {'type': str, 'required': True, 'default': 'manual', 'allowed': ['auto', 'manual']},
            'auto_threshold': {'type': int, 'required': False, 'default': 85, 'min': 0, 'max': 100},
            'auto_rescue_enabled': {'type': bool, 'required': False, 'default': True},
            'auto_rescue_max_rounds': {'type': int, 'required': False, 'default': 3, 'min': 1, 'max': 10}
        }
    },
    'qc': {
        'type': dict,
        'required': True,
        'fields': {
            'enabled': {'type': bool, 'required': False, 'default': True},
            'dimensions': {
                'type': list,
                'required': False,
                'default': ['outline_adherence', 'main_plot', 'character', 'logic', 'continuity', 'pacing', 'style']
            },
            'weights': {
                'type': dict,
                'required': False,
                'default': {
                    'outline_adherence': 20,
                    'main_plot': 15,
                    'character': 15,
                    'logic': 20,
                    'continuity': 10,
                    'pacing': 10,
                    'style': 10
                }
            },
            'pass_threshold': {'type': int, 'required': False, 'default': 85, 'min': 0, 'max': 100},
            'warning_threshold': {'type': int, 'required': False, 'default': 75, 'min': 0, 'max': 100}
        }
    },
    'backpatch': {
        'type': dict,
        'required': False,
        'default': {},
        'fields': {
            'frequency_chapters': {'type': int, 'required': False, 'default': 5, 'min': 1},
            'severity_threshold_for_block': {'type': str, 'required': False, 'default': 'high'}
        }
    },
    'time': {
        'type': dict,
        'required': False,
        'default': {},
        'fields': {
            'timezone_standard': {'type': str, 'required': False, 'default': 'Asia/Shanghai'},
            'timezone_offset': {'type': str, 'required': False, 'default': '+08:00'},
            'timestamp_format': {'type': str, 'required': False, 'default': 'ISO8601'}
        }
    },
    'run_id': {'type': str, 'required': True},
    'price_table_version': {'type': str, 'required': False, 'default': '1.0.0'},
    'created_at': {'type': str, 'required': True},
    'updated_at': {'type': str, 'required': True}
}


# ============================================================================
# Configuration Manager
# ============================================================================

class ConfigManager:
    """
    Manages book configuration lifecycle:
    - Load config from file
    - Validate against schema
    - Update with change tracking
    - Maintain update history
    """
    
    def __init__(self, run_dir: Path):
        """
        Initialize ConfigManager
        
        Args:
            run_dir: Path to run directory
        """
        self.run_dir = Path(run_dir)
        self.config_path = self.run_dir / "0-config" / "0-book-config.json"
        self._config: Optional[Dict[str, Any]] = None
        self._original_config: Optional[Dict[str, Any]] = None
    
    def load(self, force_reload: bool = False) -> Dict[str, Any]:
        """
        Load configuration from file
        
        Args:
            force_reload: Force reload even if already cached
            
        Returns:
            Configuration dictionary
            
        Raises:
            FileNotFoundError: If config file doesn't exist
            ValueError: If config is invalid JSON
        """
        if self._config is not None and not force_reload:
            return self._config
        
        if not self.config_path.exists():
            raise FileNotFoundError(f"Config file not found: {self.config_path}")
        
        with open(self.config_path, 'r', encoding='utf-8') as f:
            self._config = json.load(f)
        
        # Store original for change tracking
        self._original_config = json.loads(json.dumps(self._config))
        
        return self._config
    
    def validate(self, config: Optional[Dict[str, Any]] = None) -> Tuple[bool, List[str]]:
        """
        Validate configuration against schema
        
        Args:
            config: Config to validate (uses loaded config if None)
            
        Returns:
            Tuple of (is_valid, list_of_errors)
        """
        if config is None:
            config = self.load()
        
        errors = []
        
        def validate_field(field_name: str, field_spec: Dict, value: Any, path: str = ""):
            current_path = f"{path}.{field_name}" if path else field_name
            
            # Check required
            if field_spec.get('required', False) and value is None:
                errors.append(f"{current_path}: Required field is missing")
                return
            
            if value is None:
                return  # Optional field with None value
            
            # Check type
            expected_type = field_spec.get('type')
            if expected_type:
                if isinstance(expected_type, tuple):
                    if not isinstance(value, expected_type):
                        errors.append(f"{current_path}: Expected type {expected_type}, got {type(value)}")
                        return
                else:
                    if not isinstance(value, expected_type):
                        errors.append(f"{current_path}: Expected type {expected_type.__name__}, got {type(value).__name__}")
                        return
            
            # Check min/max for numbers
            if isinstance(value, (int, float)):
                if 'min' in field_spec and value < field_spec['min']:
                    errors.append(f"{current_path}: Value {value} is below minimum {field_spec['min']}")
                if 'max' in field_spec and value > field_spec['max']:
                    errors.append(f"{current_path}: Value {value} is above maximum {field_spec['max']}")
            
            # Check allowed values
            if 'allowed' in field_spec and value not in field_spec['allowed']:
                errors.append(f"{current_path}: Value '{value}' not in allowed values: {field_spec['allowed']}")
            
            # Recurse into nested dicts
            if isinstance(value, dict) and 'fields' in field_spec:
                for nested_name, nested_spec in field_spec['fields'].items():
                    nested_value = value.get(nested_name)
                    validate_field(nested_name, nested_spec, nested_value, current_path)
        
        # Validate all fields in schema
        for field_name, field_spec in CONFIG_SCHEMA.items():
            value = config.get(field_name)
            validate_field(field_name, field_spec, value)
        
        # Validate specific constraints
        # QC thresholds must make sense
        if 'qc' in config:
            qc = config['qc']
            if qc.get('warning_threshold', 75) >= qc.get('pass_threshold', 85):
                errors.append("qc.warning_threshold must be less than qc.pass_threshold")
        
        return len(errors) == 0, errors
    
    def update(self, updates: Dict[str, Any], reason: str = "manual_update") -> bool:
        """
        Update configuration with change tracking
        
        Args:
            updates: Dictionary of updates (supports nested paths like "book.title")
            reason: Reason for update (logged to user_interactions)
            
        Returns:
            True on success
        """
        config = self.load()
        
        # Apply updates
        for key, value in updates.items():
            if '.' in key:
                # Nested update
                parts = key.split('.')
                target = config
                for part in parts[:-1]:
                    if part not in target:
                        target[part] = {}
                    target = target[part]
                target[parts[-1]] = value
            else:
                config[key] = value
        
        # Validate
        is_valid, errors = self.validate(config)
        if not is_valid:
            print(f"[Config Error] Validation failed: {errors}")
            return False
        
        # Update timestamp
        config['updated_at'] = get_timestamp_iso()
        
        # Write atomically
        if not atomic_write_json(self.config_path, config):
            return False
        
        # Log to user_interactions
        self._log_config_change(updates, reason)
        
        # Update cache
        self._config = config
        self._original_config = json.loads(json.dumps(config))
        
        return True
    
    def _log_config_change(self, updates: Dict[str, Any], reason: str):
        """Log configuration change to user_interactions.jsonl"""
        user_interactions_path = self.run_dir / "4-state" / "user_interactions.jsonl"
        
        record = {
            'timestamp': get_timestamp_iso(),
            'type': 'setting_change',
            'trigger': reason,
            'affected_scope': 'global',
            'diff_summary': json.dumps(updates, ensure_ascii=False),
            'processed': True,
            'alignment_triggered': self._check_alignment_trigger(updates)
        }
        
        atomic_append_jsonl(user_interactions_path, record)
    
    def _check_alignment_trigger(self, updates: Dict[str, Any]) -> bool:
        """Check if update should trigger alignment check"""
        # Key fields that affect book direction
        alignment_fields = [
            'book.genre', 'book.subgenre', 'book.tone', 'book.target_word_count'
        ]
        
        for key in updates.keys():
            if any(key.startswith(field) or field.startswith(key) for field in alignment_fields):
                return True
        
        return False
    
    def get(self, key: str, default: Any = None) -> Any:
        """
        Get configuration value (supports nested keys like "book.title")
        """
        config = self.load()
        
        if '.' in key:
            parts = key.split('.')
            value = config
            for part in parts:
                if isinstance(value, dict) and part in value:
                    value = value[part]
                else:
                    return default
            return value
        
        return config.get(key, default)
    
    def has_changed(self) -> bool:
        """Check if config has unsaved changes"""
        if self._config is None or self._original_config is None:
            return False
        return json.dumps(self._config, sort_keys=True) != json.dumps(self._original_config, sort_keys=True)
    
    def get_price_table_path(self) -> Path:
        """Get path to price table file"""
        return self.run_dir / "0-config" / "price-table.json"
    
    def get_style_guide_path(self) -> Path:
        """Get path to style guide"""
        return self.run_dir / "0-config" / "style_guide.md"
    
    def get_intent_checklist_path(self) -> Path:
        """Get path to intent checklist"""
        return self.run_dir / "0-config" / "intent_checklist.json"


# ============================================================================
# Configuration Helpers
# ============================================================================

def get_model_config(config: Dict[str, Any]) -> Dict[str, Any]:
    """Extract model configuration from book config"""
    gen = config.get('generation', {})
    return {
        'model': gen.get('model', 'nvidia/moonshotai/kimi-k2.5'),
        'temperature_outline': gen.get('temperature_outline', 0.8),
        'temperature_chapter': gen.get('temperature_chapter', 0.75),
        'max_attempts': gen.get('max_attempts', 3),
        'mode': gen.get('mode', 'manual'),
        'auto_threshold': gen.get('auto_threshold', 85)
    }


def get_qc_config(config: Dict[str, Any]) -> Dict[str, Any]:
    """Extract QC configuration from book config"""
    qc = config.get('qc', {})
    return {
        'enabled': qc.get('enabled', True),
        'dimensions': qc.get('dimensions', ['outline_adherence', 'main_plot', 'character', 
                                            'logic', 'continuity', 'pacing', 'style']),
        'weights': qc.get('weights', {
            'outline_adherence': 20,
            'main_plot': 15,
            'character': 15,
            'logic': 20,
            'continuity': 10,
            'pacing': 10,
            'style': 10
        }),
        'pass_threshold': qc.get('pass_threshold', 85),
        'warning_threshold': qc.get('warning_threshold', 75)
    }


def get_book_metadata(config: Dict[str, Any]) -> Dict[str, Any]:
    """Extract book metadata from config"""
    book = config.get('book', {})
    return {
        'title': book.get('title', 'Untitled'),
        'title_slug': book.get('title_slug', 'untitled'),
        'book_uid': book.get('book_uid', ''),
        'genre': book.get('genre', ''),
        'subgenre': book.get('subgenre', ''),
        'target_word_count': book.get('target_word_count', 100000),
        'chapter_target_words': book.get('chapter_target_words', 2500),
        'language': book.get('language', 'zh'),
        'tone': book.get('tone', '轻松')
    }


# ============================================================================
# Module Test
# ============================================================================

if __name__ == "__main__":
    import tempfile
    
    print("=== Config Manager Test ===\n")
    
    with tempfile.TemporaryDirectory() as tmpdir:
        run_dir = Path(tmpdir) / "test_run"
        config_dir = run_dir / "0-config"
        config_dir.mkdir(parents=True)
        state_dir = run_dir / "4-state"
        state_dir.mkdir(parents=True)
        
        # Create test config
        test_config = {
            'version': '2.0.0',
            'book': {
                'title': '阴间外卖',
                'title_slug': 'yin_jian_wai_mai',
                'book_uid': 'a1b2c3d4',
                'genre': '都市灵异',
                'target_word_count': 250000,
                'chapter_target_words': 2500
            },
            'generation': {
                'model': 'nvidia/moonshotai/kimi-k2.5',
                'mode': 'manual',
                'max_attempts': 3
            },
            'qc': {
                'enabled': True,
                'pass_threshold': 85,
                'warning_threshold': 75
            },
            'run_id': '20260215_224500_ABC123',
            'created_at': '2026-02-15T22:45:00+08:00',
            'updated_at': '2026-02-15T22:45:00+08:00'
        }
        
        config_path = config_dir / "0-book-config.json"
        with open(config_path, 'w', encoding='utf-8') as f:
            json.dump(test_config, f, indent=2, ensure_ascii=False)
        
        # Test load
        mgr = ConfigManager(run_dir)
        config = mgr.load()
        print(f"[Test] Load config: {'PASS' if config['book']['title'] == '阴间外卖' else 'FAIL'}")
        
        # Test validate
        is_valid, errors = mgr.validate()
        print(f"[Test] Validate config: {'PASS' if is_valid else 'FAIL'}")
        if errors:
            for err in errors:
                print(f"  Error: {err}")
        
        # Test get
        title = mgr.get('book.title')
        print(f"[Test] Get nested value: {'PASS' if title == '阴间外卖' else 'FAIL'}")
        
        # Test update
        success = mgr.update({'book.tone': '暗黑'}, reason="改基调")
        print(f"[Test] Update config: {'PASS' if success else 'FAIL'}")
        
        # Verify update
        config = mgr.load(force_reload=True)
        print(f"[Test] Verify update: {'PASS' if config['book']['tone'] == '暗黑' else 'FAIL'}")
        
        # Test helpers
        model_cfg = get_model_config(config)
        print(f"[Test] Model config: {model_cfg['model']}")
        
        qc_cfg = get_qc_config(config)
        print(f"[Test] QC pass threshold: {qc_cfg['pass_threshold']}")
        
        metadata = get_book_metadata(config)
        print(f"[Test] Book title: {metadata['title']}")
        
    print("\n=== All tests completed ===")
