from __future__ import annotations

import asyncio
import inspect
import threading
from datetime import timedelta
from typing import Any, Callable, Mapping, Optional, Union

from .exceptions import SotiValidationError

IntervalValue = Union[int, float, str, timedelta]


class SotiAutopost:
    def __init__(
        self,
        client: "Soti",
        *,
        bot_id: Optional[str] = None,
        interval: IntervalValue = "15m",
        platform: Optional[str] = None,
        source: Optional[str] = None,
        immediate: bool = True,
        loop: Optional[asyncio.AbstractEventLoop] = None,
        daemon: bool = True,
    ):
        self._client = client
        self._bot_id: Optional[str] = None
        self._interval_seconds = self._parse_interval(interval)
        self._stats_provider: Optional[Callable[[], Mapping[str, Any]]] = None
        self._success_handler: Optional[Callable[[Any], Any]] = None
        self._error_handler: Optional[Callable[[BaseException], Any]] = None
        self._platform = platform.strip() if isinstance(platform, str) and platform.strip() else None
        self._source = source.strip() if isinstance(source, str) and source.strip() else None
        self._immediate = bool(immediate)
        self._loop = loop
        self._daemon = bool(daemon)
        self._stop_event = threading.Event()
        self._thread: Optional[threading.Thread] = None
        self.last_error: Optional[BaseException] = None

        if bot_id is not None:
            self.set_bot_id(bot_id)

    def init_stats(self, provider: Callable[[], Mapping[str, Any]]) -> "SotiAutopost":
        if not callable(provider):
            raise SotiValidationError("init_stats provider must be callable")
        self._stats_provider = provider
        return self

    def on_success(self, handler: Callable[[Any], Any]) -> "SotiAutopost":
        if handler is not None and not callable(handler):
            raise SotiValidationError("on_success handler must be callable")
        self._success_handler = handler
        return self

    def on_error(self, handler: Callable[[BaseException], Any]) -> "SotiAutopost":
        if handler is not None and not callable(handler):
            raise SotiValidationError("on_error handler must be callable")
        self._error_handler = handler
        return self

    def every(self, interval: IntervalValue) -> "SotiAutopost":
        self._interval_seconds = self._parse_interval(interval)
        if self.is_running:
            self.stop()
            self.start()
        return self

    def interval(self, interval: IntervalValue) -> "SotiAutopost":
        return self.every(interval)

    def set_bot_id(self, bot_id: str) -> "SotiAutopost":
        self._bot_id = self._client._validate_snowflake(bot_id, field_name="bot_id")
        return self

    def bot(self, bot_id: str) -> "SotiAutopost":
        return self.set_bot_id(bot_id)

    def set_platform(self, platform: Optional[str]) -> "SotiAutopost":
        self._platform = platform.strip() if isinstance(platform, str) and platform.strip() else None
        return self

    def set_source(self, source: Optional[str]) -> "SotiAutopost":
        self._source = source.strip() if isinstance(source, str) and source.strip() else None
        return self

    def set_immediate(self, immediate: bool) -> "SotiAutopost":
        self._immediate = bool(immediate)
        return self

    @property
    def is_running(self) -> bool:
        return bool(self._thread and self._thread.is_alive())

    def start(self, bot_id: Optional[str] = None) -> "SotiAutopost":
        if bot_id is not None:
            self.set_bot_id(bot_id)

        self._assert_ready()

        if self.is_running:
            return self

        self._stop_event.clear()
        self._thread = threading.Thread(target=self._worker, daemon=self._daemon)
        self._thread.start()
        return self

    def stop(self) -> "SotiAutopost":
        self._stop_event.set()
        if self._thread and self._thread.is_alive():
            self._thread.join(timeout=2.0)
        self._thread = None
        return self

    def post_once(self, bot_id: Optional[str] = None) -> Any:
        if bot_id is not None:
            self.set_bot_id(bot_id)

        self._assert_ready()

        try:
            stats_payload = self._run_callable(self._stats_provider)
            if not isinstance(stats_payload, Mapping):
                raise SotiValidationError("stats provider must return a dict-like object")

            payload = dict(stats_payload)
            if self._platform and payload.get("platform") is None:
                payload["platform"] = self._platform
            if self._source and payload.get("source") is None:
                payload["source"] = self._source

            result = self._client.post_bot_stats(self._bot_id, payload)

            if self._success_handler:
                self._run_callable(self._success_handler, result)

            return result
        except BaseException as exc:
            self.last_error = exc
            if self._error_handler:
                self._run_callable(self._error_handler, exc)
                return None
            raise

    def _worker(self) -> None:
        if self._immediate:
            self._safe_post_once()

        while not self._stop_event.wait(self._interval_seconds):
            self._safe_post_once()

    def _safe_post_once(self) -> None:
        try:
            self.post_once()
        except BaseException as exc:
            self.last_error = exc

    def _assert_ready(self) -> None:
        if not self._bot_id:
            raise SotiValidationError("bot_id is required")
        if not callable(self._stats_provider):
            raise SotiValidationError("init_stats provider is required")

    def _run_callable(self, fn: Callable[..., Any], *args: Any) -> Any:
        result = fn(*args)
        if inspect.isawaitable(result):
            return self._await_result(result)
        return result

    def _await_result(self, awaitable: Any) -> Any:
        if self._loop and self._loop.is_running():
            future = asyncio.run_coroutine_threadsafe(awaitable, self._loop)
            return future.result()

        try:
            running_loop = asyncio.get_running_loop()
        except RuntimeError:
            running_loop = None

        if running_loop and running_loop.is_running():
            raise SotiValidationError("async callback requires loop parameter in autopost")

        return asyncio.run(awaitable)

    @staticmethod
    def _parse_interval(value: IntervalValue) -> float:
        if isinstance(value, timedelta):
            seconds = value.total_seconds()
            if seconds < 1:
                raise SotiValidationError("interval must be >= 1 second")
            return float(seconds)

        if isinstance(value, (int, float)):
            if value < 1:
                raise SotiValidationError("interval must be >= 1 second")
            return float(value)

        text = str(value or "").strip().lower()
        if not text:
            raise SotiValidationError("interval is required")

        units = {
            "s": 1,
            "m": 60,
            "h": 3600,
            "d": 86400,
        }

        unit = text[-1]
        if unit not in units:
            raise SotiValidationError("interval string must end with s, m, h, or d")

        number_text = text[:-1].strip()
        if not number_text:
            raise SotiValidationError("interval value is invalid")

        try:
            amount = float(number_text)
        except ValueError as exc:
            raise SotiValidationError("interval value is invalid") from exc

        seconds = amount * units[unit]
        if seconds < 1:
            raise SotiValidationError("interval must be >= 1 second")

        return float(seconds)
