from __future__ import annotations

import asyncio
import base64
import json
import pickle
from pathlib import Path
from typing import Any, Literal

import yaml
from pydantic import BaseModel, Field

from oneiric.actions.metadata import ActionMetadata
from oneiric.actions.payloads import normalize_payload
from oneiric.core.lifecycle import LifecycleError
from oneiric.core.logging import get_logger
from oneiric.core.resolution import CandidateSource


class SerializationActionSettings(BaseModel):
    default_format: Literal["json", "yaml", "pickle"] = Field(
        default="json",
        description="Default serialization format when payload omits one.",
    )
    sort_keys: bool = Field(
        default=False,
        description="Sort keys for deterministic JSON/YAML output.",
    )
    ensure_ascii: bool = Field(
        default=False,
        description="Force ASCII output for JSON encoding.",
    )


class SerializationAction:
    metadata = ActionMetadata(
        key="serialization.encode",
        provider="builtin-serialization",
        factory="oneiric.actions.serialization: SerializationAction",
        description="Serializes/deserializes payloads across JSON, YAML, and pickle",
        domains=["task", "service", "workflow"],
        capabilities=["encode", "decode", "serialize"],
        stack_level=30,
        priority=430,
        source=CandidateSource.LOCAL_PKG,
        owner="Platform Core",
        requires_secrets=False,
        side_effect_free=True,
        settings_model=SerializationActionSettings,
    )

    _TEXT_FORMATS = {"json", "yaml"}

    def __init__(self, settings: SerializationActionSettings | None = None) -> None:
        self._settings = settings or SerializationActionSettings()
        self._logger = get_logger("action.serialization")

    async def execute(self, payload: dict | None = None) -> dict:
        payload = normalize_payload(payload)
        mode = (payload.get("mode") or "encode").lower()
        fmt = (payload.get("format") or self._settings.default_format).lower()
        if fmt not in {"json", "yaml", "pickle"}:
            raise LifecycleError("serialization-unsupported-format")

        if fmt == "pickle":
            self._logger.warning(
                "pickle-format-security-warning",
                message="Using pickle format - ensure data is from trusted sources only",
                mode=mode,
            )

        if mode == "encode":
            return await self._encode(fmt, payload)
        if mode == "decode":
            return await self._decode(fmt, payload)
        raise LifecycleError("serialization-invalid-mode")

    async def _encode(self, fmt: str, payload: dict) -> dict:  # noqa: C901
        value = payload.get("value")
        if value is None and "data" in payload:
            value = payload["data"]
        if value is None:
            raise LifecycleError("serialization-value-required")
        sort_keys = payload.get("sort_keys")
        if sort_keys is None:
            sort_keys = self._settings.sort_keys
        ensure_ascii = payload.get("ensure_ascii")
        if ensure_ascii is None:
            ensure_ascii = self._settings.ensure_ascii
        if fmt == "json":
            text = json.dumps(value, ensure_ascii=ensure_ascii, sort_keys=sort_keys)
            data_bytes = text.encode("utf-8")
        elif fmt == "yaml":
            text = yaml.safe_dump(value, sort_keys=sort_keys)
            data_bytes = text.encode("utf-8")
        else:
            text = None

            # Pickle is intentionally used for Python object serialization in trusted workflows. For untrusted data, use json/yaml formats instead.
            data_bytes = pickle.dumps(
                value, protocol=pickle.HIGHEST_PROTOCOL
            )  # nosemgrep: python.lang.security.deserialization.pickle.avoid-pickle
        await self._maybe_write_path(payload.get("path"), data_bytes, fmt)
        self._logger.info("serialization-action-encoded", fmt=fmt, mode="encode")
        result = {
            "status": "encoded",
            "format": fmt,
        }
        if fmt in self._TEXT_FORMATS:
            result["text"] = text if text is not None else data_bytes.decode("utf-8")
        else:
            result["encoding"] = "base64"
            result["data"] = base64.b64encode(data_bytes).decode("ascii")
        return result

    async def _decode(self, fmt: str, payload: dict) -> dict:
        raw = await self._resolve_source(fmt, payload)
        if fmt == "json":
            text = raw.decode("utf-8")
            data = json.loads(text)
        elif fmt == "yaml":
            text = raw.decode("utf-8")
            data = yaml.safe_load(text)
        else:
            # Pickle is intentionally used for Python object serialization in trusted workflows. For untrusted data, use json/yaml formats instead.
            data = pickle.loads(
                raw
            )  # nosemgrep: python.lang.security.deserialization.pickle.avoid-pickle
        self._logger.info("serialization-action-decoded", fmt=fmt, mode="decode")
        return {
            "status": "decoded",
            "format": fmt,
            "data": data,
        }

    async def _resolve_source(self, fmt: str, payload: dict) -> bytes:
        if path := payload.get("path"):
            return await self._read_path(path, fmt)

        value = self._extract_value_from_payload(payload)
        if value is None:
            raise LifecycleError("serialization-source-required")

        if fmt in self._TEXT_FORMATS:
            return self._process_text_value(value)

        return self._process_binary_value(value)

    def _extract_value_from_payload(self, payload: dict) -> Any:
        value = payload.get("data")
        if value is None and "text" in payload:
            value = payload["text"]
        if value is None and "value" in payload:
            value = payload["value"]
        return value

    def _process_text_value(self, value: Any) -> bytes:
        if isinstance(value, bytes):
            return value
        if isinstance(value, str):
            return value.encode("utf-8")
        raise LifecycleError("serialization-text-required")

    def _process_binary_value(self, value: Any) -> bytes:
        if isinstance(value, bytes):
            return value
        if isinstance(value, str):
            try:
                return base64.b64decode(value.encode("ascii"))
            except Exception as exc:  # pragma: no cover - invalid base64 path
                raise LifecycleError("serialization-b64-invalid") from exc
        raise LifecycleError("serialization-binary-required")

    async def _maybe_write_path(self, path_value: Any, data: bytes, fmt: str) -> None:
        if not path_value:
            return
        path = Path(path_value)
        if fmt in self._TEXT_FORMATS:
            text = data.decode("utf-8")
            await asyncio.to_thread(path.write_text, text, encoding="utf-8")
        else:
            await asyncio.to_thread(path.write_bytes, data)

    async def _read_path(self, path_value: Any, fmt: str) -> bytes:
        path = Path(path_value)
        if fmt in self._TEXT_FORMATS:
            text = await asyncio.to_thread(path.read_text, encoding="utf-8")
            return text.encode("utf-8")
        return await asyncio.to_thread(path.read_bytes)


__all__ = ["SerializationAction", "SerializationActionSettings"]
