import json
from uuid import uuid4
from typing import List, Any, Optional, Dict, Union, Iterator

from pydantic import BaseModel, ConfigDict, field_validator, Field

from phi.assistant import Assistant


class Task(BaseModel):
    # -*- Task settings
    # Task name
    name: Optional[str] = None
    # Task UUID (autogenerated if not set)
    task_id: Optional[str] = Field(None, validate_default=True)
    # Task description
    description: Optional[str] = None

    # Assistant to run this task
    assistant: Optional[Assistant] = None
    # Reviewer for this task. Set reviewer=True for a default reviewer
    reviewer: Optional[Union[Assistant, bool]] = None

    # -*- Task Output
    # Final output of this Task
    output: Optional[Any] = None
    # If True, shows the output of the task in the workflow.run() function
    show_output: bool = True
    # Save the output to a file
    save_output_to_file: Optional[str] = None

    # Cached values: do not set these directly
    _assistant: Optional[Assistant] = None

    model_config = ConfigDict(arbitrary_types_allowed=True)

    @field_validator("task_id", mode="before")
    def set_task_id(cls, v: Optional[str]) -> str:
        return v if v is not None else str(uuid4())

    @property
    def streamable(self) -> bool:
        return self.get_assistant().streamable

    def get_task_output_as_str(self) -> Optional[str]:
        if self.output is None:
            return None

        if isinstance(self.output, str):
            return self.output

        if issubclass(self.output.__class__, BaseModel):
            # Convert current_task_message to json if it is a BaseModel
            return self.output.model_dump_json(exclude_none=True, indent=2)

        try:
            return json.dumps(self.output, indent=2)
        except Exception:
            return str(self.output)
        finally:
            return None

    def get_assistant(self) -> Assistant:
        if self._assistant is None:
            self._assistant = self.assistant or Assistant()
        return self._assistant

    def _run(
        self,
        message: Optional[Union[List, Dict, str]] = None,
        *,
        stream: bool = True,
        **kwargs: Any,
    ) -> Iterator[str]:
        assistant = self.get_assistant()
        assistant.task = self.description

        assistant_output = ""
        if stream and self.streamable:
            for chunk in assistant.run(message=message, stream=True, **kwargs):
                assistant_output += chunk if isinstance(chunk, str) else ""
                if self.show_output:
                    yield chunk if isinstance(chunk, str) else ""
        else:
            assistant_output = assistant.run(message=message, stream=False, **kwargs)  # type: ignore

        self.output = assistant_output
        if self.save_output_to_file:
            fn = self.save_output_to_file.format(name=self.name, task_id=self.task_id)
            with open(fn, "w") as f:
                f.write(self.output)

        # -*- Yield task output if not streaming
        if not stream:
            if self.show_output:
                yield self.output
            else:
                yield ""

    def run(
        self,
        message: Optional[Union[List, Dict, str]] = None,
        *,
        stream: bool = True,
        **kwargs: Any,
    ) -> Union[Iterator[str], str, BaseModel]:
        if stream and self.streamable:
            resp = self._run(message=message, stream=True, **kwargs)
            return resp
        else:
            resp = self._run(message=message, stream=False, **kwargs)
            return next(resp)
