from typing import Any, Annotated

from annotated_doc import Doc
from clm_core.types import SDCompressionConfig, FieldImportance, CLMOutput


class SDEncoder:
    ROW_SEPARATOR = "|"
    NESTED_OPEN = "["
    NESTED_CLOSE = "]"
    SCHEMA_OPEN = "{"
    SCHEMA_CLOSE = "}"

    def __init__(
        self,
        *,
        config: Annotated[
            SDCompressionConfig,
            Doc(
                "Configuration controlling field selection, truncation, and compression strategy."
            ),
        ],
        catalog_name: Annotated[
            str, Doc("Logical name for the catalog; currently informational only.")
        ] = "CATALOG",
        delimiter: Annotated[
            str, Doc("Character used to separate field values inside bracket tokens.")
        ] = ",",
    ):
        self._config = config
        self._catalog_name = catalog_name
        self._done: bool = False
        self._delimiter = delimiter

    @property
    def delimiter(self) -> str:
        return self._delimiter

    @delimiter.setter
    def delimiter(self, val: str) -> None:
        self._delimiter = val

    def encode(
        self,
        catalog: Annotated[
            list[dict[str, Any]] | dict[str, Any],
            Doc("A single item dict or a list of item dicts to compress."),
        ],
    ) -> CLMOutput:
        """
        Main entry point: compress entire catalog

        Args:
            catalog: List of catalog items (any schema)

        Returns:
            Compressed catalog in format:
            {field1,field2,field3}[value1,value2,value3][value1,value2,value3]
        """
        if isinstance(catalog, dict):
            compressed_item = self._format_item_token(self.encode_item(catalog))
            return CLMOutput(
                component="ds_compression",
                compressed=compressed_item,
                original=catalog,
                metadata={
                    "original_length": len(catalog),
                    "compressed_length": len(compressed_item),
                },
            )

        compressed_results = [
            self.encode_item(item) for item in catalog if isinstance(item, dict)
        ]

        if len(compressed_results) > 0:
            compressed_keys = self._format_header_keys(compressed_results[0])

            parts = [f"{{{compressed_keys}}}"]

            for compressed in compressed_results:
                item_token = self._format_item_token(compressed)
                parts.append(item_token)

            compressed_item = "".join(parts)
            return CLMOutput(
                component="ds_compression",
                compressed=compressed_item,
                original=catalog,
                metadata={
                    "original_length": len(catalog),
                    "compressed_length": len(compressed_item),
                },
            )
        return CLMOutput(
            component="ds_compression",
            compressed="",
            original=catalog,
            metadata={
                "original_length": len(catalog),
                "compressed_length": 0,
            },
        )

    def encode_item(
        self,
        item: Annotated[dict[str, Any], Doc("A single catalog item dict to compress.")],
    ) -> dict[str, Any]:
        """Compress a single catalog item."""
        compressed: dict[str, Any] = {}

        if "id" in item:
            compressed["id"] = item["id"]

        for key, value in item.items():
            if not self._should_include_field(key, value):
                continue
            if isinstance(value, dict):
                compressed[key] = self.encode_item(value)
            elif isinstance(value, list):
                compressed[key] = [
                    self.encode_item(v) if isinstance(v, dict) else v for v in value
                ]
            else:
                compressed[key] = value
        return compressed

    def _get_ordered_fields(
        self,
        compressed_item: Annotated[
            dict[str, Any],
            Doc("A compressed item dict whose fields should be ordered for output."),
        ],
    ) -> list[tuple[str, Any]]:
        """
        Get fields ordered consistently: simple fields (sorted by default_fields_order) first,
        then complex fields.

        Returns:
            List of (key, value) tuples in the correct order
        """
        simple_fields = []
        complex_fields = []

        for key, value in compressed_item.items():
            key_lower = key.lower()
            if key_lower in self._config.simple_fields:
                simple_fields.append((key_lower, value))
            else:
                complex_fields.append((key_lower, value))

        simple_fields.sort(
            key=lambda x: self._config.default_fields_order.index(x[0])
            if x[0] in self._config.default_fields_order
            else 999
        )

        return simple_fields + complex_fields

    def _format_item_token(
        self,
        compressed_item: Annotated[
            dict[str, Any], Doc("Dict returned by encode_item().")
        ],
    ) -> str:
        """
        Format single item as a bracketed token with delimiter-separated values.

        Args:
            compressed_item: Dict returned by encode_item()

        Returns:
            Token string like "[ID-001,Product Name,Category,199.99]"
        """
        ordered_fields = self._get_ordered_fields(compressed_item)
        parts = []

        for key, value in ordered_fields:
            max_len = None
            if key.lower() not in self._config.simple_fields:
                max_len = self._config.max_truncation_length

            formatted_value = self._format_value(value, max_length=max_len)
            if formatted_value and formatted_value != "":
                parts.append(formatted_value)

        return f"[{f'{self.delimiter}'.join(parts)}]"

    def _format_header_keys(
        self,
        compressed_item: Annotated[
            dict[str, Any],
            Doc("The first compressed item whose keys define the header schema."),
        ],
    ) -> str:
        """Format header keys in the same order as values are formatted."""
        ordered_fields = self._get_ordered_fields(compressed_item)
        return self.delimiter.join(key for key, _ in ordered_fields)

    def _format_value(
        self,
        value: Annotated[
            Any, Doc("Any value from a compressed item (str, int, list, dict, etc.).")
        ],
        max_length: Annotated[
            int | None,
            Doc(
                "Optional maximum character length; the value is truncated with '...' if exceeded."
            ),
        ] = None,
    ) -> str:
        """Format a value for token output — minimal transformation for max compression."""
        if isinstance(value, list):
            if value and isinstance(value[0], dict):
                rows = [self._format_inline_object(v) for v in value]
                return self.ROW_SEPARATOR.join(rows)
            return "+".join(str(v) for v in value)

        if isinstance(value, dict):
            return self._format_inline_object(value)

        if isinstance(value, str):
            result = value.replace(self._delimiter, ";")
        else:
            result = str(value)

        if max_length and len(result) > max_length:
            result = result[:max_length] + "..."
        return result

    def _format_inline_object(
        self,
        obj: Annotated[
            dict[str, Any], Doc("A nested dict to encode inline as {schema}[values].")
        ],
    ) -> str:
        ordered = self._get_ordered_fields(obj)

        schema = self.delimiter.join(k for k, _ in ordered)
        values = self.delimiter.join(self._format_value(v) for _, v in ordered)

        return (
            f"{self.SCHEMA_OPEN}{schema}{self.SCHEMA_CLOSE}"
            f"{self.NESTED_OPEN}{values}{self.NESTED_CLOSE}"
        )

    def _should_include_field(
        self,
        key: Annotated[str, Doc("The field name to evaluate for inclusion.")],
        value: Annotated[
            Any,
            Doc(
                "The field value; used for heuristic importance detection when auto_detect is enabled."
            ),
        ],
    ) -> bool:
        """Decide if a field should be included based on config rules and auto-detection heuristics."""
        if self._config.excluded_fields and key in self._config.excluded_fields:
            return False

        if self._config.required_fields and key in self._config.required_fields:
            return True

        if self._config.field_importance and key in self._config.field_importance:
            return (
                self._config.field_importance[key] >= self._config.importance_threshold
            )

        if self._config.auto_detect:
            importance = self._detect_field_importance(key, value)
            return importance.value >= self._config.importance_threshold
        return True

    def _detect_field_importance(
        self,
        key: Annotated[
            str, Doc("The field name; matched against default importance patterns.")
        ],
        value: Annotated[
            Any,
            Doc("The field value; empty/None values are assigned NEVER importance."),
        ],
    ) -> FieldImportance:
        """Automatically detect how important a field is based on its name and value heuristics."""
        key_lower = key.lower()

        for pattern, importance in self._config.default_fields_importance.items():
            if pattern in key_lower:
                return importance

        if key_lower.startswith("internal_") or key_lower.startswith("_"):
            return FieldImportance.LOW

        if key_lower.endswith("_at") or key_lower.endswith("_date"):
            return FieldImportance.NEVER

        if value is None or value == "":
            return FieldImportance.NEVER

        if isinstance(value, str):
            if len(value) > 500:
                return FieldImportance.MEDIUM
            if len(value) < 3:
                return FieldImportance.LOW
        return FieldImportance.MEDIUM
