# yaml_reader.py
import copy
from pathlib import Path
from typing import List, Dict, Any, Type, Optional, Tuple

from cerberus import Validator
from ruamel.yaml import YAML, YAMLError
from ruamel.yaml.comments import CommentedMap, CommentedSeq

def to_plain_dict(data: Any) -> Any:
    """
    Recursively converts ruamel.yaml's CommentedMap/Seq to plain dicts/lists.
    This is essential for compatibility with libraries like Cerberus.
    """
    if isinstance(data, CommentedMap):
        return {key: to_plain_dict(value) for key, value in data.items()}
    if isinstance(data, CommentedSeq):
        return [to_plain_dict(item) for item in data]
    return data


class ConfigLoader:
    def __init__(
            self, schema: Dict = None,
            error_modifications: Optional[List[Dict]] = None,
            validator_class: Type[Validator] = Validator
    ):
        """Initializes the ConfigLoader."""
        self.schema = schema
        self.validator_class = validator_class
        self.error_modifications = error_modifications or ERROR_MODIFICATION_RULES
        self.ruamel_parser = YAML(typ='safe')

    def read(
            self,
            config_file: Path,
            allow_unknown: bool = False,
            root_node: str | None = None,
            block_descriptor: str | None = None,
            normalize: bool = False
    ) -> dict:
        """Loads and validates a YAML configuration file against the schema."""
        if not isinstance(config_file, Path):
            config_file = Path(config_file)
        if not config_file.exists():
            raise FileNotFoundError(f"Configuration file not found: {Path.cwd()}/{config_file}")

        yaml_text = config_file.read_text(encoding="utf-8")

        validated_data, line, error_text = self._validate_text(
            yaml_text, normalize=normalize, allow_unknown=allow_unknown
        )

        if error_text:
            error_location = f"on or near line {line} " if line else ""
            error_message = (
                f"  ❌ Error: Configuration file '{config_file}' has an error {error_location}:\n\n"
                f"    {error_text}"
            )
            raise ValueError(error_message)

        return validated_data

    def validate_text(
            self,
            yaml_text: str,
            normalize: bool = False,
            allow_unknown: bool = False
    ) -> Tuple[Optional[Dict], Optional[int], Optional[str]]:
        """Public method to validate a string of YAML text for GUI use."""
        return self._validate_text(yaml_text, normalize=normalize, allow_unknown=allow_unknown)

    def _validate_text(
            self,
            yaml_text: str,
            normalize: bool = False,
            allow_unknown: bool = False
    ) -> Tuple[Optional[Dict], Optional[int], Optional[str]]:
        """
        Internal method that contains the core validation logic for a YAML string.
        """
        try:
            # Step 1: Load with ruamel to get line numbers and comment-aware data
            ruamel_data = self.ruamel_parser.load(yaml_text) or {}
        except YAMLError as e:
            line = e.problem_mark.line + 1 if hasattr(e, 'problem_mark') else None
            problem, suggestion = self._translate_error_message(str(e))
            error_message = f"Problem: YAML syntax error.\n    {problem}\n    Suggestion: {suggestion}"
            return None, line, error_message

        if self.schema:
            # --- THE DEFINITIVE FIX ---
            # Step 2: Convert the ruamel_data to a plain dict for Cerberus
            plain_data_for_validation = to_plain_dict(ruamel_data)

            # Step 3: Validate the plain dict using a fresh validator and a copied schema
            schema_copy = copy.deepcopy(self.schema)
            validator = self.validator_class(schema_copy)
            validator.allow_unknown = allow_unknown

            if not validator.validate(plain_data_for_validation):
                field, issues = next(iter(validator.errors.items()))
                path_list = field.split('.')
                issue = issues[0]

                # Step 4: Use the original ruamel_data to find the line number
                error_node = self._get_node_by_path(ruamel_data, path_list)
                line = error_node.lc.line + 1 if hasattr(error_node, 'lc') else None

                problem, suggestion = self._translate_cerberus_issue(validator, issue, path_list)
                found_value = self._get_node_by_path(ruamel_data, path_list)
                error_message = (
                    f"Problem: {problem}\n"
                    f"    Found Value: {repr(found_value)}\n"
                    f"    Suggestion: {suggestion}"
                )
                return None, line, error_message

            # Return the plain, normalized data on success
            return validator.normalized(plain_data_for_validation) if normalize else plain_data_for_validation, None, None

        # If no schema, return the plain data
        return to_plain_dict(ruamel_data), None, None

    # ... (the rest of the file is unchanged) ...
    def _translate_error_message(self, error_string: str) -> tuple[str, str]:
        for rule in self.error_modifications:
            trigger_phrase = rule.get("trigger_phrase", "")
            if trigger_phrase and trigger_phrase in error_string:
                return rule["problem"], rule["suggestion"]
        return f"An unknown error occurred: {error_string}", "Please check file syntax."

    def _translate_cerberus_issue(self, validator: Validator, issue: str, path: List[str]) -> tuple[str, str]:
        field_name = path[-1] if path else "a setting"
        problem, suggestion = self._translate_error_message(issue)
        problem = problem.format(field_name=field_name)
        suggestion = suggestion.format(field_name=field_name)

        if "unallowed value" in issue:
            schema_rule = self._get_schema_rule_from_path(validator.schema, path)
            if schema_rule and 'allowed' in schema_rule:
                allowed_values = ", ".join(f"'{v}'" for v in schema_rule['allowed'])
                suggestion += f" Allowed values are: {allowed_values}."
        return problem, suggestion

    def _get_schema_rule_from_path(self, schema: Any, path: List[str]) -> Optional[Dict]:
        current_schema_item = schema
        for key in path:
            if not isinstance(current_schema_item, dict): return None
            current_schema_item = current_schema_item.get(key)
            if current_schema_item is None: return None
            if isinstance(current_schema_item, dict) and 'schema' in current_schema_item:
                current_schema_item = current_schema_item['schema']
        return current_schema_item if isinstance(current_schema_item, dict) else None

    @staticmethod
    def _get_node_by_path(data: Any, path: List[str]) -> Any:
        current = data
        for key in path:
            try:
                if isinstance(current, list) and key.isdigit():
                    current = current[int(key)]
                elif hasattr(current, 'get'):
                    current = current.get(key)
                else:
                    return None
            except (KeyError, IndexError, TypeError):
                return None
        return current


ERROR_MODIFICATION_RULES = [
    # --- Rules for modifying  ruamel.yaml error text ---
    {
        "key": "YAML_MAPPING_ERROR",
        "trigger_phrase": "expected '<block end>', but found '<block mapping start>'",
        "problem": "Indentation or colon error.",
        "suggestion": "Check for a missing colon (:) after a key or incorrect indentation for the items below it."
    },
    # ... other ruamel.yaml error rules ...

    # --- Rules for modifying Cerberus  error text ---
    {
        "key": "REQUIRED_FIELD",
        "trigger_phrase": "required field",
        "problem": "The required setting '{field_name}' is missing.",
        "suggestion": "Please add '{field_name}: <value>' to this section.",
    },
    {
        "key": "UNKNOWN_FIELD",
        "trigger_phrase": "unknown field",
        "problem": "The setting '{field_name}' is not recognized here.",
        "suggestion": "Please remove it or check for a typo.",
    },
    {
        "key": "UNALLOWED_VALUE",
        "trigger_phrase": "unallowed value",
        "problem": "The value for '{field_name}' is not allowed.",
        "suggestion": "Please choose one of the permitted values."
    },
    {
        "key": "NULL_VALUE",
        "trigger_phrase": "null value not allowed",
        "problem": "The setting '{field_name}' cannot be empty.",
        "suggestion": "Please provide a value for '{field_name}'."
    }
]