# src/chatiss_open_sdk/core/format_response.py
from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Optional, cast

from .time import parse_iso_z
from ..exceptions import ProtocolError
from ..types import (
    APIStatus,
    TokenResult,
    APIResponse,
    TongueEventResult,
    TongueCreateResult,
    TongueDataItem,
    TongueIndicator,
    TongueNmpaAskingResultItem,
    TongueNmpaTongueResultItem,
    TongueWord,
    WordArticleDetailResult,
    WordArticleParagraph,
)


@dataclass(frozen=True)
class Envelope:
    status: APIStatus
    code: Optional[int]
    msg: Optional[str]
    result: Any


def parse_envelope(*, method: str, endpoint: str, data: Any) -> Envelope:
    """Parse unified API envelope. FAIL is not an exception; only schema violations raise ProtocolError."""
    if not isinstance(data, dict):
        raise ProtocolError(
            message=f"Invalid response type, expected object(dict) but got {type(data).__name__}",
            method=method,
            endpoint=endpoint,
            body=data,
        )

    status_raw = data.get("status")
    if status_raw not in ("SUCCESS", "FAIL"):
        raise ProtocolError(
            message=f"Invalid status value: {status_raw!r}",
            method=method,
            endpoint=endpoint,
            body=data,
        )
    status = cast(APIStatus, status_raw)

    code_raw = data.get("code")
    if code_raw is None:
        code = None
    else:
        try:
            code = int(code_raw)
        except Exception as e:
            raise ProtocolError(
                message=f"Invalid code type/value: {code_raw!r}",
                method=method,
                endpoint=endpoint,
                body=data,
            ) from e

    msg_raw = data.get("msg")
    msg = str(msg_raw) if msg_raw is not None else None

    return Envelope(status=status, code=code, msg=msg, result=data.get("result"))


def require_dict_result(*, method: str, endpoint: str, env: Envelope) -> dict[str, Any]:
    """On SUCCESS, ensure env.result is a dict."""
    if env.status != "SUCCESS":
        return {}
    if not isinstance(env.result, dict):
        raise ProtocolError(
            message=f"Invalid result type, expected object(dict) but got {type(env.result).__name__}",
            method=method,
            endpoint=endpoint,
            body=env.result,
        )
    return env.result


def format_response_get_jwt_token(*, method: str, endpoint: str, data: Any) -> APIResponse[TokenResult]:
    env = parse_envelope(method=method, endpoint=endpoint, data=data)

    token_result: TokenResult | None = None
    if env.status == "SUCCESS":
        raw = require_dict_result(method=method, endpoint=endpoint, env=env)

        token = raw.get("token")
        expiration = raw.get("expiration")
        if not token or not expiration:
            raise ProtocolError(
                message=f"Missing required fields in result: token/expiration, result={raw!r}",
                method=method,
                endpoint=endpoint,
                body=raw,
            )

        try:
            token_result = TokenResult(
                token=str(token),
                expiration=parse_iso_z(str(expiration)),
            )
        except Exception as e:
            raise ProtocolError(
                message=f"Failed to parse token result: {e}",
                method=method,
                endpoint=endpoint,
                body=raw,
            ) from e

    return APIResponse(
        status=env.status,
        result=token_result,
        code=env.code,
        msg=env.msg,
    )


def format_response_word_article_detail(
    *,
    method: str,
    endpoint: str,
    data: Any,
) -> APIResponse[WordArticleDetailResult]:
    env = parse_envelope(method=method, endpoint=endpoint, data=data)

    article_result: WordArticleDetailResult | None = None
    if env.status == "SUCCESS":
        raw = require_dict_result(method=method, endpoint=endpoint, env=env)

        article_id = raw.get("id")
        paragraphs_raw = raw.get("paragraphs")

        if not isinstance(article_id, str):
            raise ProtocolError(
                message=f"Invalid id type, expected str but got {type(article_id).__name__}",
                method=method,
                endpoint=endpoint,
                body=raw,
            )

        if not isinstance(paragraphs_raw, list):
            raise ProtocolError(
                message=f"Invalid paragraphs type, expected list but got {type(paragraphs_raw).__name__}",
                method=method,
                endpoint=endpoint,
                body=raw,
            )

        paragraphs: list[WordArticleParagraph] = []
        for item in paragraphs_raw:
            if not isinstance(item, dict):
                raise ProtocolError(
                    message=f"Invalid paragraph item type, expected dict but got {type(item).__name__}",
                    method=method,
                    endpoint=endpoint,
                    body=item,
                )

            title = item.get("title")
            content = item.get("content")
            if not isinstance(title, str):
                raise ProtocolError(
                    message=f"Invalid paragraph title type, expected str but got {type(title).__name__}",
                    method=method,
                    endpoint=endpoint,
                    body=item,
                )

            if not isinstance(content, str):
                raise ProtocolError(
                    message=f"Invalid paragraph content type, expected str but got {type(content).__name__}",
                    method=method,
                    endpoint=endpoint,
                    body=item,
                )

            paragraphs.append(
                WordArticleParagraph(
                    title=title,
                    content=content,
                )
            )

        article_result = WordArticleDetailResult(
            id=article_id,
            paragraphs=paragraphs,
        )

    return APIResponse(
        status=env.status,
        result=article_result,
        code=env.code,
        msg=env.msg,
    )


def _ensure_optional_str(*, value: Any, field_name: str, method: str, endpoint: str, body: Any) -> Optional[str]:
    if value is None:
        return None
    if not isinstance(value, str):
        raise ProtocolError(
            message=f"Invalid {field_name} type, expected str or null but got {type(value).__name__}",
            method=method,
            endpoint=endpoint,
            body=body,
        )
    return value


def _ensure_str(*, value: Any, field_name: str, method: str, endpoint: str, body: Any) -> str:
    if not isinstance(value, str):
        raise ProtocolError(
            message=f"Invalid {field_name} type, expected str but got {type(value).__name__}",
            method=method,
            endpoint=endpoint,
            body=body,
        )
    return value


def _ensure_optional_bool(*, value: Any, field_name: str, method: str, endpoint: str, body: Any) -> Optional[bool]:
    if value is None:
        return None
    if not isinstance(value, bool):
        raise ProtocolError(
            message=f"Invalid {field_name} type, expected bool or null but got {type(value).__name__}",
            method=method,
            endpoint=endpoint,
            body=body,
        )
    return value


def _ensure_bool(*, value: Any, field_name: str, method: str, endpoint: str, body: Any) -> bool:
    if not isinstance(value, bool):
        raise ProtocolError(
            message=f"Invalid {field_name} type, expected bool but got {type(value).__name__}",
            method=method,
            endpoint=endpoint,
            body=body,
        )
    return value


def _ensure_optional_float(*, value: Any, field_name: str, method: str, endpoint: str, body: Any) -> Optional[float]:
    if value is None:
        return None
    if isinstance(value, bool) or not isinstance(value, (int, float)):
        raise ProtocolError(
            message=f"Invalid {field_name} type, expected number or null but got {type(value).__name__}",
            method=method,
            endpoint=endpoint,
            body=body,
        )
    return float(value)


def _ensure_float(*, value: Any, field_name: str, method: str, endpoint: str, body: Any) -> float:
    if isinstance(value, bool) or not isinstance(value, (int, float)):
        raise ProtocolError(
            message=f"Invalid {field_name} type, expected number but got {type(value).__name__}",
            method=method,
            endpoint=endpoint,
            body=body,
        )
    return float(value)


def _parse_tongue_words(*, value: Any, field_name: str, method: str, endpoint: str, body: Any) -> list[TongueWord]:
    if not isinstance(value, list):
        raise ProtocolError(
            message=f"Invalid {field_name} type, expected list but got {type(value).__name__}",
            method=method,
            endpoint=endpoint,
            body=body,
        )

    words: list[TongueWord] = []
    for item in value:
        if not isinstance(item, dict):
            raise ProtocolError(
                message=f"Invalid {field_name} item type, expected dict but got {type(item).__name__}",
                method=method,
                endpoint=endpoint,
                body=item,
            )
        words.append(
            TongueWord(
                article_id=_ensure_optional_str(
                    value=item.get("article_id"),
                    field_name=f"{field_name}.article_id",
                    method=method,
                    endpoint=endpoint,
                    body=item,
                ),
                title=_ensure_str(
                    value=item.get("title"),
                    field_name=f"{field_name}.title",
                    method=method,
                    endpoint=endpoint,
                    body=item,
                ),
            )
        )
    return words


def _parse_tongue_create_result(*, method: str, endpoint: str, raw: dict[str, Any]) -> TongueCreateResult:
    nmpa_tongue_raw = raw.get("nmpa_tongue_result")
    if not isinstance(nmpa_tongue_raw, list):
        raise ProtocolError(
            message=f"Invalid nmpa_tongue_result type, expected list but got {type(nmpa_tongue_raw).__name__}",
            method=method,
            endpoint=endpoint,
            body=raw,
        )

    nmpa_tongue_result: list[TongueNmpaTongueResultItem] = []
    for item in nmpa_tongue_raw:
        if not isinstance(item, dict):
            raise ProtocolError(
                message=f"Invalid nmpa_tongue_result item type, expected dict but got {type(item).__name__}",
                method=method,
                endpoint=endpoint,
                body=item,
            )
        nmpa_tongue_result.append(
            TongueNmpaTongueResultItem(
                zone=_ensure_str(
                    value=item.get("zone"),
                    field_name="nmpa_tongue_result.zone",
                    method=method,
                    endpoint=endpoint,
                    body=item,
                ),
                words=_parse_tongue_words(
                    value=item.get("words"),
                    field_name="nmpa_tongue_result.words",
                    method=method,
                    endpoint=endpoint,
                    body=item,
                ),
            )
        )

    nmpa_asking_raw = raw.get("nmpa_asking_result")
    if not isinstance(nmpa_asking_raw, list):
        raise ProtocolError(
            message=f"Invalid nmpa_asking_result type, expected list but got {type(nmpa_asking_raw).__name__}",
            method=method,
            endpoint=endpoint,
            body=raw,
        )

    nmpa_asking_result: list[TongueNmpaAskingResultItem] = []
    for item in nmpa_asking_raw:
        if not isinstance(item, dict):
            raise ProtocolError(
                message=f"Invalid nmpa_asking_result item type, expected dict but got {type(item).__name__}",
                method=method,
                endpoint=endpoint,
                body=item,
            )
        nmpa_asking_result.append(
            TongueNmpaAskingResultItem(
                category=_ensure_str(
                    value=item.get("category"),
                    field_name="nmpa_asking_result.category",
                    method=method,
                    endpoint=endpoint,
                    body=item,
                ),
                words=_parse_tongue_words(
                    value=item.get("words"),
                    field_name="nmpa_asking_result.words",
                    method=method,
                    endpoint=endpoint,
                    body=item,
                ),
            )
        )

    tongue_data_raw = raw.get("tongue_data")
    if not isinstance(tongue_data_raw, list):
        raise ProtocolError(
            message=f"Invalid tongue_data type, expected list but got {type(tongue_data_raw).__name__}",
            method=method,
            endpoint=endpoint,
            body=raw,
        )

    tongue_data: list[TongueDataItem] = []
    for item in tongue_data_raw:
        if not isinstance(item, dict):
            raise ProtocolError(
                message=f"Invalid tongue_data item type, expected dict but got {type(item).__name__}",
                method=method,
                endpoint=endpoint,
                body=item,
            )

        indicators_raw = item.get("indicators")
        if not isinstance(indicators_raw, list):
            raise ProtocolError(
                message=f"Invalid indicators type, expected list but got {type(indicators_raw).__name__}",
                method=method,
                endpoint=endpoint,
                body=item,
            )

        indicators: list[TongueIndicator] = []
        for indicator in indicators_raw:
            if not isinstance(indicator, dict):
                raise ProtocolError(
                    message=f"Invalid indicator item type, expected dict but got {type(indicator).__name__}",
                    method=method,
                    endpoint=endpoint,
                    body=indicator,
                )
            indicators.append(
                TongueIndicator(
                    name=_ensure_str(
                        value=indicator.get("name"),
                        field_name="tongue_data.indicators.name",
                        method=method,
                        endpoint=endpoint,
                        body=indicator,
                    ),
                    value=_ensure_float(
                        value=indicator.get("value"),
                        field_name="tongue_data.indicators.value",
                        method=method,
                        endpoint=endpoint,
                        body=indicator,
                    ),
                    status=_ensure_str(
                        value=indicator.get("status"),
                        field_name="tongue_data.indicators.status",
                        method=method,
                        endpoint=endpoint,
                        body=indicator,
                    ),
                    status_note=_ensure_str(
                        value=indicator.get("status_note"),
                        field_name="tongue_data.indicators.status_note",
                        method=method,
                        endpoint=endpoint,
                        body=indicator,
                    ),
                )
            )

        tongue_data.append(
            TongueDataItem(
                indicators=indicators,
                zone=_ensure_str(
                    value=item.get("zone"),
                    field_name="tongue_data.zone",
                    method=method,
                    endpoint=endpoint,
                    body=item,
                ),
                viscera=_ensure_str(
                    value=item.get("viscera"),
                    field_name="tongue_data.viscera",
                    method=method,
                    endpoint=endpoint,
                    body=item,
                ),
            )
        )

    return TongueCreateResult(
        id=_ensure_str(value=raw.get("id"), field_name="id", method=method, endpoint=endpoint, body=raw),
        is_valid=_ensure_bool(
            value=raw.get("is_valid"),
            field_name="is_valid",
            method=method,
            endpoint=endpoint,
            body=raw,
        ),
        nmpa_tongue_result=nmpa_tongue_result,
        nmpa_asking_result=nmpa_asking_result,
        tongue_data=tongue_data,
        invalid_code=_ensure_optional_str(
            value=raw.get("invalid_code"),
            field_name="invalid_code",
            method=method,
            endpoint=endpoint,
            body=raw,
        ),
        invalid_reason=_ensure_optional_str(
            value=raw.get("invalid_reason"),
            field_name="invalid_reason",
            method=method,
            endpoint=endpoint,
            body=raw,
        ),
    )


def format_response_tongue_create(*, method: str, endpoint: str, evt: Any) -> tuple[str, TongueEventResult]:
    event_name = str(evt.get("event", ""))
    raw = evt.get("data")

    if event_name == "done":
        event_result = TongueEventResult(
            status="SUCCESS",
            result={"raw": raw},
            code=None,
            msg=None,
        )
        return event_name, event_result

    env = parse_envelope(method=method, endpoint=endpoint, data=raw)

    result = env.result
    if env.status == "SUCCESS":
        parsed_raw = require_dict_result(method=method, endpoint=endpoint, env=env)
        result = _parse_tongue_create_result(method=method, endpoint=endpoint, raw=parsed_raw)

    event_result = TongueEventResult(
        status=env.status,
        result=result,
        code=env.code,
        msg=env.msg,
    )

    return event_name, event_result
