from typing import Optional, Iterator

from urllib.parse import quote as url_quote
from pathlib import PurePath
from uuid import uuid4

from .request import Requester, Request, Response
from .parser import DavParser
from .error import ApiError


class TaskList:
    """Handle of a task list returned by the API."""

    def __init__(self, href: PurePath, name: Optional[str]) -> None:
        self._href: PurePath = href
        self._name: Optional[str] = name

    @property
    def href(self) -> PurePath:
        return self._href

    @property
    def name(self) -> Optional[str]:
        return self._name

    @name.setter
    def name(self, name: str) -> None:
        self._name = name


class TaskFile:
    """Handle of a particular task of a task list returned by the API."""

    def __init__(self, href: PurePath, etag: Optional[str], content: str) -> None:
        self._href: PurePath = href
        self._etag: Optional[str] = etag
        self._content: str = content  # vCalendar/vEvent/iCal payload

    @property
    def href(self) -> PurePath:
        return self._href

    @property
    def etag(self) -> Optional[str]:
        return self._etag

    @property
    def content(self) -> str:
        return self._content

    @content.setter
    def content(self, content: str) -> None:
        self._content = content


class NextcloudTasksApi:
    """Main interface, using a backend for XML DAV requests and working with ICAL data."""

    def __init__(self, requester: Requester) -> None:
        self._requester: Requester = requester
        self._parser: DavParser = DavParser()

    @classmethod
    def _make_href(cls, href: str) -> PurePath:
        """Treat all 'remote.php' API URLs as relative to configured base URL, even when returned absolute."""
        return PurePath(href.lstrip("/") if href.startswith("/remote.php/") else href)

    def get_lists(self) -> Iterator[TaskList]:
        """Find all available task lists."""

        response: Response = self._requester.request(Request(
            method="PROPFIND",
            url="remote.php/dav/calendars/{}/".format(url_quote(self._requester.username, safe='')),
            headers={"Content-Type": "application/xml; charset=utf-8"},
            content=self._parser.get_propfind_calendars(),
        ))
        response.raise_for(207, "application/xml; charset=utf-8")

        for href, name in self._parser.parse_list_for_calendars(response.content):
            yield TaskList(href=self._make_href(href), name=name)

    def get_list(self, task_list: TaskList, completed: Optional[bool] = None) -> Iterator[TaskFile]:
        """Get all tasks of a tasklist, optionally filter remotely by completion state for performance reasons."""

        response: Response = self._requester.request(Request(
            method="REPORT",
            url=task_list.href.as_posix(),
            headers={"Content-Type": "application/xml; charset=utf-8", "Depth": "1"},
            content=self._parser.get_report_calendar(completed),
        ))
        response.raise_for(207, "application/xml; charset=utf-8")

        for href, etag, caldav in self._parser.parse_get_for_calendar(response.content):
            yield TaskFile(href=self._make_href(href), etag=etag, content=caldav)

    def update_list(self, task_list: TaskList) -> None:
        """Update a list with its updated handle."""

        response: Response = self._requester.request(Request(
            method="PROPPATCH",
            url=task_list.href.as_posix(),
            headers={"Content-Type": "application/xml; charset=utf-8", "Depth": "0"},
            content=self._parser.get_property_update(task_list.name if task_list.name is not None
                                                     else task_list.href.stem)
        ))
        response.raise_for(207, "application/xml; charset=utf-8")
        if list(self._make_href(_)
                for _ in self._parser.parse_propstat_for_status(response.content)) != [task_list.href]:
            raise ApiError(f"Cannot update tasklist '{task_list.href}'") from None

    def delete_list(self, task_list: TaskList) -> None:
        """Delete the given list."""

        response: Response = self._requester.request(Request(
            method="DELETE",
            headers={"Depth": "0"},
            url=task_list.href.as_posix(),
        ))
        response.raise_for(204)
        response.content.close()

    def create_list(self, filename: str, name: Optional[str] = None) -> TaskList:
        """Create a new list with the given (unverified) filename and displayname."""

        url: PurePath = PurePath("remote.php/dav/calendars") / \
            url_quote(self._requester.username, safe='') / \
            url_quote(filename, safe='')

        response: Response = self._requester.request(Request(
            method="MKCOL",
            url=url.as_posix(),
            headers={"Content-Type": "application/xml; charset=utf-8", "Depth": "0"},
            content=self._parser.get_mkcol_calendar(name if name is not None else filename),
        ))
        response.raise_for(201)
        response.content.close()
        return TaskList(href=url, name=name if name is not None else filename)

    def update(self, task: TaskFile) -> TaskFile:
        """Update a task with its updated content."""

        response: Response = self._requester.request(Request(
            method="PUT",
            url=task.href.as_posix(),
            headers={**{"Content-Type": "text/calendar; component=vtodo; charset=utf-8"},
                     **({"If-Match": task.etag} if task.etag is not None else {})},
            content=task.content.encode(encoding="utf-8", errors="strict"),
        ))
        response.raise_for(204)
        response.content.close()
        return self._get_task(task.href)  # for new etag

    def delete(self, task: TaskFile) -> None:
        """Delete a task."""

        response: Response = self._requester.request(Request(
            method="DELETE",
            headers={"If-Match": task.etag} if task.etag is not None else {},
            url=task.href.as_posix(),
        ))
        response.raise_for(204)
        response.content.close()

    def create(self, task_list: TaskList, task: str) -> TaskFile:
        """Create a new task in the task list with the given iCal content."""

        href: PurePath = task_list.href / PurePath(str(uuid4()).upper()).with_suffix(".ics")
        response: Response = self._requester.request(Request(
            method="PUT",
            url=href.as_posix(),
            headers={"Content-Type": "text/calendar; charset=utf-8"},
            content=task.encode(encoding="utf-8", errors="strict"),
        ))
        response.raise_for(201)
        response.content.close()
        return self._get_task(href)

    def _get_task(self, task_href: PurePath) -> TaskFile:
        response: Response = self._requester.request(Request(
            method="PROPFIND",
            url=task_href.as_posix(),
            headers={"Content-Type": "application/xml; charset=utf-8"},
            content=self._parser.get_propfind_calendar(),
        ))
        response.raise_for(207, "application/xml; charset=utf-8")

        try:
            href, etag, caldav = next(self._parser.parse_get_for_calendar(response.content))
            response.content.close()
            return TaskFile(href=self._make_href(href), etag=etag, content=caldav)
        except StopIteration:
            raise ApiError(f"Cannot get task '{task_href}'") from None