import time
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable, Generic, TypeVar, get_args, get_origin

from ovld import Medley, ovld

from ..ctx import Context, WorkingDirectory
from ..instructions import BaseInstruction, strip
from ..priority import HI1
from ..proxy import ProxyBase
from ..tell import tells
from .partial import Partial


@dataclass(frozen=True)
class DefaultFactory(BaseInstruction):
    factory: Callable


T = TypeVar("T")
MISSING = object()


class FileBacked(Generic[T]):
    path: Path
    value_type: type
    serieux: object
    context: Context
    timestamp: float = None
    refresh: bool = False
    default_factory: Callable = None
    value: T = None

    def __init__(
        self,
        path: Path,
        value_type: type,
        serieux: object,
        context: Context,
        refresh: bool = False,
        default_factory: Callable = None,
    ):
        self.path = path
        value_type = Partial.strip(value_type)
        self.value_type = value_type
        self.serieux = serieux
        self.context = context
        self.refresh = refresh
        stripped = strip(value_type)
        self.default_factory = default_factory or get_origin(stripped) or stripped
        self._value = None
        self.timestamp = None
        self.load()

    @property
    def value(self):
        if self.refresh and self.path.exists():
            file_mtime = self.path.stat().st_mtime
            if file_mtime > self.timestamp:
                self.load()
        return self._value

    def load(self):
        if self.path.exists():
            self._value = self.serieux.deserialize(self.value_type, self.path, self.context)
            self.timestamp = self.path.stat().st_mtime
        elif self.default_factory:
            self._value = self.default_factory()
            self.timestamp = time.time()
        else:  # pragma: no cover
            raise FileNotFoundError(self.path)

    def save(self, new_value=MISSING):
        if new_value is not MISSING:
            self._value = new_value
        self.serieux.dump(self.value_type, self._value, self.context, dest=self.path)
        self.timestamp = time.time()

    def __str__(self):
        return f"{self._value}@{self.path}"

    __repr__ = __str__


@dataclass(frozen=True)
class FileBackedOptions(BaseInstruction):
    default_factory: Callable | None = None
    refresh: bool = False


@dataclass(frozen=True)
class FileProxy(FileBackedOptions):
    pass


class FileBackedProxy(ProxyBase):
    __special_attributes__ = {
        *ProxyBase.__special_attributes__,
        "_wrapper",
        "_path",
        "load",
        "save",
    }

    def __init__(self, path, value_type, *args, **kwargs):
        self._wrapper = FileBacked(path, value_type, *args, **kwargs)
        self._type = value_type

    @property
    def _obj(self):
        return self._wrapper.value

    @property
    def _path(self):
        return self._wrapper.path

    def load(self):
        return self._wrapper.load()

    def save(self, new_value=MISSING):
        return self._wrapper.save(new_value)

    def __str__(self):
        return str(self._wrapper)

    __repr__ = __str__


PRIO = HI1.next()


def _to_path(obj, ctx):
    if isinstance(ctx, WorkingDirectory):  # pragma: no cover
        return ctx.directory / obj
    else:
        return Path(obj)


class FileBackedFeature(Medley):
    @ovld(priority=PRIO)
    def serialize(self, t: type[FileBacked], obj: FileBacked, ctx):
        return str(obj.path)

    @ovld(priority=PRIO)
    def serialize(self, t: type[Any @ FileProxy], obj: FileBackedProxy, ctx: Context):
        return str(obj._wrapper.path)

    @ovld(priority=PRIO)
    def deserialize(
        self, t: type[FileBacked] | type[FileBacked @ FileBackedOptions], obj: Path | str, ctx
    ):
        cls, fopt = FileBackedOptions.decompose(t)
        (vt,) = get_args(strip(cls))
        extra = vars(fopt) if fopt else {}
        return cls(
            path=_to_path(obj, ctx),
            value_type=vt,
            serieux=self,
            context=ctx,
            **extra,
        )

    @ovld(priority=PRIO)
    def deserialize(self, t: type[Any @ FileProxy], obj: str | Path, ctx: Context):
        # Merging doesn't make sense here so Partial will just cause problems
        t = Partial.strip(t)
        value_type, fb = FileProxy.decompose(t)
        return FileBackedProxy(
            _to_path(obj, ctx),
            value_type,
            self,
            ctx,
            default_factory=fb.default_factory,
            refresh=fb.refresh,
        )

    @ovld(priority=PRIO)
    def schema(self, t: type[FileBacked], ctx):
        return {"type": "string"}

    @ovld(priority=PRIO)
    def schema(self, t: type[Any @ FileProxy], ctx: Context):
        return {"type": "string"}


@tells.register(priority=HI1.next())
def _(expected: type[Any @ FileProxy], given: type[str]):
    return set()


@tells.register(priority=HI1.next())
def _(expected: type[FileBacked], given: type[str]):
    return set()
