# SPDX-License-Identifier: Apache-2.0.
# Copyright (c) 2024 - 2025 Waldiez and contributors.

"""Files related request handler."""

import json
import logging
from pathlib import Path
from typing import Any

import anyio.to_thread

from waldiez.exporter import WaldiezExporter
from waldiez.models import Waldiez

from .models import (
    ConvertWorkflowRequest,
    ConvertWorkflowResponse,
    SaveFlowRequest,
    SaveFlowResponse,
)


class FileRequestHandler:
    """Handles file-related requests."""

    @staticmethod
    async def handle_save_request(
        msg: SaveFlowRequest,
        workspace_dir: Path,
        client_id: str,
        logger: logging.Logger,
    ) -> dict[str, Any]:
        """Handle save flow request.

        Parameters
        ----------
        msg : SaveFlowRequest
            The save flow request message.
        workspace_dir : Path
            The workspace directory.
        client_id : str
            The client ID.
        logger : logging.Logger
            The logger instance.

        Returns
        -------
        dict[str, Any]
            The response dictionary.
        """
        return await anyio.to_thread.run_sync(
            FileRequestHandler._handle_save_request,
            msg,
            workspace_dir,
            client_id,
            logger,
        )

    @staticmethod
    def _handle_save_request(
        msg: SaveFlowRequest,
        workspace_dir: Path,
        client_id: str,
        logger: logging.Logger,
    ) -> dict[str, Any]:
        path = msg.path or f"waldiez_{client_id}.waldiez"
        try:
            output_path = resolve_output_path(
                path,
                workspace_dir=workspace_dir,
                expected_ext="waldiez",
            )
        except ValueError as exc:
            logger.error("Error resolving output path: %s", exc)
            return SaveFlowResponse.fail(
                error=f"Invalid output path: {exc}",
                path=path,
            ).model_dump(mode="json")
        # pylint: disable=too-many-try-statements
        try:
            if output_path.exists() and not msg.force:
                return SaveFlowResponse.fail(
                    error=f"File exists: {output_path}",
                    file_path=str(output_path.relative_to(workspace_dir)),
                ).model_dump(mode="json")

            # Parent dir already created by resolve_output_path
            output_path.write_text(msg.data, encoding="utf-8", newline="\n")

            return SaveFlowResponse.ok(
                path=str(output_path.relative_to(workspace_dir))
            ).model_dump(mode="json")
        except Exception as e:  # pylint: disable=broad-exception-caught
            logger.error("Error saving flow: %s", e)
            return SaveFlowResponse.fail(error=str(e)).model_dump(mode="json")

    @staticmethod
    async def handle_convert_request(
        msg: ConvertWorkflowRequest,
        client_id: str,
        workspace_dir: Path,
        logger: logging.Logger,
    ) -> dict[str, Any]:
        """Handle a convert workflow request.

        Parameters
        ----------
        msg : ConvertWorkflowRequest
            The convert workflow request message.
        client_id : str
            The client ID.
        workspace_dir : Path
            The workspace directory.
        logger : logging.Logger
            The logger instance.

        Returns
        -------
        dict[str, Any]
            The response dictionary.
        """
        return await anyio.to_thread.run_sync(
            FileRequestHandler._handle_convert_request,
            msg,
            client_id,
            workspace_dir,
            logger,
        )

    @staticmethod
    def _handle_convert_request(
        msg: ConvertWorkflowRequest,
        client_id: str,
        workspace_dir: Path,
        logger: logging.Logger,
    ) -> dict[str, Any]:
        """Handle a convert workflow request.

        Parameters
        ----------
        msg : ConvertWorkflowRequest
            The convert workflow request message.
        client_id : str
            The client ID.
        workspace_dir : Path
            The workspace directory.
        logger : logging.Logger
            The logger instance.

        Returns
        -------
        dict[str, Any]
            The response dictionary.
        """
        target_format = (msg.format or "").strip().lower()
        if target_format not in {"py", "ipynb"}:
            return ConvertWorkflowResponse.fail(
                error=f"Unsupported target format: {target_format}",
                format=target_format,
            ).model_dump(mode="json")

        try:
            waldiez_data = Waldiez.from_dict(json.loads(msg.data))
        except Exception as e:  # pylint: disable=broad-exception-caught
            return ConvertWorkflowResponse.fail(
                error=f"Invalid flow_data: {e}",
                format=target_format,
            ).model_dump(mode="json")

        try:
            # Use normalized target_format for default name
            path = msg.path or f"waldiez_{client_id}.{target_format}"
            output_path = resolve_output_path(
                path,
                workspace_dir=workspace_dir,
                expected_ext=target_format,
            )
        except ValueError as exc:
            logger.error("Error resolving output path: %s", exc)
            return ConvertWorkflowResponse.fail(
                error=f"Invalid output path: {exc}",
                format=target_format,
            ).model_dump(mode="json")

        try:
            exporter = WaldiezExporter(waldiez_data)
            exporter.export(path=output_path, force=True, structured_io=True)

            return ConvertWorkflowResponse.ok(
                format=target_format,
                path=str(output_path.relative_to(workspace_dir)),
            ).model_dump(mode="json")
        except Exception as e:  # pylint: disable=broad-exception-caught
            logger.error("Error converting workflow: %s", e)
            return ConvertWorkflowResponse.fail(
                error=str(e), format=target_format
            ).model_dump(mode="json")


def resolve_output_path(
    filename: str,
    workspace_dir: Path,
    expected_ext: str | None = None,
) -> Path:
    """
    Resolve output path inside the workspace.

    Parameters
    ----------
    filename : str
        Provided filename (may be relative or absolute).
    workspace_dir : Path
        The workspace directory to resolve the output path against.
    expected_ext : str | None
        If provided, ensure the filename ends with this extension.

    Returns
    -------
    Path
        Resolved absolute path, with parent directories created.

    Raises
    ------
    ValueError
        If the output path is outside the workspace.
    """
    # Normalize workspace_dir to an absolute path
    workspace_dir = workspace_dir.resolve()

    output_path = Path(filename)
    if not output_path.is_absolute():
        output_path = workspace_dir / output_path

    if expected_ext and output_path.suffix != f".{expected_ext}":
        output_path = output_path.with_suffix(f".{expected_ext}")

    output_path = output_path.resolve()

    # Ensure output_path is a subpath of workspace_dir
    try:
        output_path.relative_to(workspace_dir)
    except ValueError as exc:
        raise ValueError(
            f"Output path {output_path} is outside workspace {workspace_dir}"
        ) from exc

    output_path.parent.mkdir(parents=True, exist_ok=True)
    return output_path
