"""API Call node - HTTP requests with Jinja templating."""

import json
import logging
from functools import cached_property
from json import JSONDecodeError
from typing import Any, ClassVar, Type

import httpx
from overrides import override
from pydantic import Field
from workflow_engine import (
    Data,
    ExecutionContext,
    FieldSchemaMappingValue,
    FloatValue,
    IntegerValue,
    JSONValue,
    Node,
    NodeTypeInfo,
    Params,
    StringMapValue,
    StringValue,
    ValidationContext,
    Value,
    WorkflowException,
)
from workflow_engine.core import StakeholderLevel
from workflow_engine.files import JSONFileValue, JSONLinesFileValue, TextFileValue

from ..utils import format_jinja

logger = logging.getLogger(__name__)


class APICallParams(Params):
    url: StringValue = Field(
        title="URL",
        description="The API endpoint URL.",
    )
    method: StringValue = Field(
        title="Method",
        description="The HTTP method to use.",
        default=StringValue("GET"),
    )
    headers: StringMapValue[StringValue] = Field(
        title="Headers",
        description=(
            "HTTP headers to include in the request. Values support Jinja "
            "substitution from the node's parameters (e.g. "
            "`Bearer {{ api_token }}`)."
        ),
        default=StringMapValue({}),
    )
    body_template: StringValue = Field(
        title="Body Template",
        description="The request body template.",
        default=StringValue(""),
    )
    parameters: FieldSchemaMappingValue = Field(
        title="Parameters",
        description="Parameters used in URL, header, and body templates.",
        default=FieldSchemaMappingValue({}),
    )
    timeout: FloatValue = Field(
        title="Timeout",
        description="Request timeout in seconds.",
        default=FloatValue(30.0),
    )


class APICallOutput(Data):
    status_code: IntegerValue = Field(
        title="Status Code",
        description="The HTTP status code of the response.",
    )
    response: JSONValue = Field(
        title="Response",
        description="The response data.",
    )
    headers: StringMapValue[StringValue] = Field(
        title="Headers",
        description="HTTP headers of the response.",
    )


class APICallNode(
    Node[
        Data,
        APICallOutput,
        APICallParams,
    ]
):
    """Node that makes HTTP API calls with Jinja templating for URL and request body."""

    TYPE_INFO: ClassVar[NodeTypeInfo] = NodeTypeInfo.from_parameter_type(
        display_name="API Call",
        description=(
            "Makes HTTP API calls with Jinja templating for URL, headers, "
            "and request body."
        ),
        version="0.4.0",
        parameter_type=APICallParams,
    )

    @cached_property
    def supports_json_output(self) -> bool:
        content_type = self.params.headers.get("Accept")
        return content_type is not None and "application/json" in content_type

    @override
    async def dynamic_input_type(
        self,
        context: ValidationContext,
    ) -> Type[Data]:
        schema = self.params.parameters.to_data_schema("APICallInput")
        return schema.build_data_cls()

    @classmethod
    @override
    def static_output_type(cls) -> Type[APICallOutput]:
        return APICallOutput

    async def _expand_parameter_value(
        self, context: ExecutionContext, value: Value
    ) -> Any:
        """Expand data files into their content for templating."""
        assert isinstance(value, Value)
        if isinstance(value, JSONFileValue):
            try:
                return await value.read_data(context)
            except Exception as e:
                raise WorkflowException(
                    f"Failed to read JSON file '{value.path}'.",
                    level=StakeholderLevel.USER,
                ) from e
        elif isinstance(value, JSONLinesFileValue):
            try:
                return await value.read_data(context)
            except Exception as e:
                raise WorkflowException(
                    f"Failed to read JSON lines file '{value.path}'.",
                    level=StakeholderLevel.USER,
                ) from e
        elif isinstance(value, TextFileValue):
            try:
                return await value.read_text(context)
            except Exception as e:
                raise WorkflowException(
                    f"Failed to read text file '{value.path}'.",
                    level=StakeholderLevel.USER,
                ) from e
        return value.root

    def _render_template(
        self,
        template: str,
        parameters: dict[str, Any],
        *,
        label: str,
    ) -> str:
        try:
            return format_jinja(template, parameters)
        except Exception as e:
            raise WorkflowException(
                f"Failed to format {label}: {e}",
                level=StakeholderLevel.USER,
            ) from e

    def _is_json_response(self, response: httpx.Response) -> bool:
        content_type = response.headers.get("content-type", "")
        assert isinstance(content_type, str)
        content_type = content_type.lower()
        return "application/json" in content_type or "text/json" in content_type

    def _parse_response_data(self, response: httpx.Response) -> Any:
        try:
            if self._is_json_response(response):
                return response.json()
            else:
                return response.text
        except JSONDecodeError:
            try:
                return response.text
            except Exception:
                return str(response.content)
        except Exception:
            return str(response.content)

    @override
    async def run(
        self,
        *,
        context: ExecutionContext,
        input_type: type[Data],
        output_type: type[APICallOutput],
        input: Data,
    ) -> APICallOutput:
        # Prepare template parameters from runtime input (field names come from
        # params.parameters schemas — same pattern as JinjaNode).
        parameters: dict[str, Any] = {}
        if len(self.params.parameters) > 0:
            for name in self.params.parameters.keys():
                if hasattr(input, name):
                    parameters[name] = await self._expand_parameter_value(
                        context, getattr(input, name)
                    )
                else:
                    raise WorkflowException(
                        f"Parameter {name} is not set.",
                        level=StakeholderLevel.USER,
                    )

        # Format URL with Jinja templating
        url = self._render_template(
            self.params.url.root,
            parameters,
            label="URL template",
        )

        # Format request body with Jinja templating
        request_body = None
        body_template = self.params.body_template.root
        if body_template.strip():
            body_text = self._render_template(
                body_template,
                parameters,
                label="request body template",
            )
            if body_text.strip().startswith(("{", "[")):
                try:
                    request_body = json.loads(body_text)
                except JSONDecodeError:
                    request_body = body_text
            else:
                request_body = body_text

        # Prepare headers (values are Jinja-templated; names are fixed)
        headers: dict[str, str] = {}
        for key, value in self.params.headers.items():
            headers[key] = self._render_template(
                value.root,
                parameters,
                label=f"header {key!r}",
            )

        if request_body is not None and isinstance(request_body, (dict, list)):
            if "content-type" not in {k.lower() for k in headers.keys()}:
                headers["Content-Type"] = "application/json"

        logger.info(f"Making {self.params.method} request to: {url}")

        # Make the HTTP request
        timeout = float(self.params.timeout.root)
        method = self.params.method.root
        try:
            async with httpx.AsyncClient(timeout=timeout) as client:
                if isinstance(request_body, (dict, list)):
                    response = await client.request(
                        method=method,
                        url=url,
                        headers=headers,
                        json=request_body,
                    )
                elif request_body is not None:
                    response = await client.request(
                        method=method,
                        url=url,
                        headers=headers,
                        content=request_body,
                    )
                else:
                    response = await client.request(
                        method=method, url=url, headers=headers
                    )
        except httpx.TimeoutException as e:
            raise WorkflowException(
                f"Request timed out after {timeout} seconds.",
                level=StakeholderLevel.USER,
            ) from e
        except httpx.RequestError as e:
            raise WorkflowException(
                f"Request failed: {e}",
                level=StakeholderLevel.USER,
            ) from e
        except Exception as e:
            raise WorkflowException(
                f"Unexpected error during API call: {e}",
                level=StakeholderLevel.USER,
            ) from e

        # Parse response
        status_code = IntegerValue(response.status_code)
        response_data = self._parse_response_data(response)
        response_headers = StringMapValue[StringValue](
            {k: StringValue(v) for k, v in response.headers.items()}
        )

        return output_type(
            status_code=status_code,
            response=response_data,
            headers=response_headers,
        )


__all__ = (
    "APICallNode",
    "APICallParams",
)
