import hashlib
import base64
from datetime import datetime, date
from typing import Any, Dict, List, Optional, Sequence, Union
from hashids import Hashids

class HashService:
    def __init__(self, hashids_instance: Optional[Union[Hashids, Any]] = None):
        """
        :param hashids_instance: a hashids.Hashids instance or any object with .encode()/.decode()
        """
        self.hashids = hashids_instance or Hashids()

    # -------------------------
    # Public API
    # -------------------------
    def get_hashed_data_string(self, value: str) -> str:
        """
        Equivalent to createHash('sha256').update(value,'utf8').digest('base64')
        """
        h = hashlib.sha256()
        h.update(value.encode("utf-8"))
        digest_bytes = h.digest()
        return base64.b64encode(digest_bytes).decode("utf-8")

    def hash_response(self, hash_config: Any, response: Any, properties: Sequence[str], dateproperties: Optional[Sequence[str]] = None) -> Any:
        """
        Top-level wrapper to hash properties in `response`.
        :param hash_config: hashids instance or object with encode/decode to use
        :param response: python object (dict/list/scalar)
        :param properties: list of keys to hash (for objects)
        :param dateproperties: optional list of date keys (currently unused beyond passing along)
        :returns: transformed response
        """
        try:
            self.hashids = hash_config or self.hashids
            if properties is None or response is None:
                raise ValueError("properties and response must be provided")
            return self._hash(response, properties, dateproperties)
        except Exception:
            raise

    def unhash_request(self, hash_config: Any, request: Any, properties: Sequence[str]) -> Any:
        """
        Top-level wrapper to unhash properties in `request`.
        """
        try:
            self.hashids = hash_config or self.hashids
            if properties is None or request is None:
                raise ValueError("properties and request must be provided")
            return self._unhash(request, properties)
        except Exception:
            raise

    # -------------------------
    # Internal recursive logic
    # -------------------------
    def _unhash(self, request: Any, properties: Sequence[str]) -> Any:
        """Recursively un-hash values in request according to properties list."""
        if request is None or isinstance(request, (int, float, bool)):
            return request

        if isinstance(request, str):
            # scalar string -> nothing to unhash unless top-level caller requested that key
            return request

        if isinstance(request, list):
            return self._deep_unhashed_array(request, properties)

        if isinstance(request, dict):
            return self._deep_unhashed_object(request, properties)

        # for other types (datetime, date, etc), return as-is
        return request

    def _hash(self, response: Any, properties: Sequence[str], dateproperties: Optional[Sequence[str]] = None) -> Any:
        """Recursively hash values in response according to properties list."""
        if response is None or isinstance(response, (int, float, bool)):
            return response

        if isinstance(response, str):
            return response

        if isinstance(response, list):
            return self._deep_hashed_array(response, properties, dateproperties)

        if isinstance(response, dict):
            return self._deep_hashed_object(response, properties, dateproperties)

        # datetime/date values: return as-is
        if isinstance(response, (datetime, date)):
            return response

        return response

    # -------------------------
    # Deep traversal helpers
    # -------------------------
    def _deep_unhashed_object(self, request: Dict[str, Any], properties: Sequence[str]) -> Dict[str, Any]:
        result: Dict[str, Any] = {}
        for key, value in request.items():
            # If the key is in the properties to be unhashed and value is a string -> decode
            if key in properties and isinstance(value, str):
                try:
                    decoded = self.hashids.decode(value)
                    # hashids.decode returns a tuple/list of ints in python; return first element or tuple
                    if isinstance(decoded, (list, tuple)):
                        result[key] = decoded[0] if len(decoded) > 0 else None
                    else:
                        result[key] = decoded
                except Exception:
                    # if decode fails, propagate original value (or raise if you prefer)
                    result[key] = value
            # elif UtilsCommon.is_input_datetime(value):
            #     # keep date-like strings/datetimes as-is (TS kept them)
            #     result[key] = value
            # elif UtilsCommon.is_input_date(value):
            #     result[key] = value
            else:
                # Recurse
                result[key] = self._unhash(value, properties)
        return result

    def _deep_unhashed_array(self, collection: List[Any], properties: Sequence[str]) -> List[Any]:
        result = []
        for item in collection:
            result.append(self._unhash(item, properties))
        return result

    def _deep_hashed_object(self, response: Dict[str, Any], properties: Sequence[str], dateproperties: Optional[Sequence[str]] = None) -> Dict[str, Any]:
        result: Dict[str, Any] = {}
        for key, value in response.items():
            if key in properties:
                # Only hash numeric values (the TS code hashed numbers)
                if value is not None and value != "" and value != 0 and isinstance(value, (int,)):
                    try:
                        # hashids.encode expects integers
                        result[key] = self.hashids.encode(int(value))
                    except Exception:
                        # fallback: keep original if encode fails
                        result[key] = value
                else:
                    result[key] = value
            else:
                # if it's a datetime, keep as-is (TS included timezone conversions; we keep raw)
                if isinstance(value, (datetime, date)):
                    result[key] = value
                else:
                    result[key] = self._hash(value, properties, dateproperties)
        return result

    def _deep_hashed_array(self, collection: List[Any], properties: Sequence[str], dateproperties: Optional[Sequence[str]] = None) -> List[Any]:
        result = []
        for item in collection:
            result.append(self._hash(item, properties, dateproperties))
        return result
