import os
from datetime import datetime
from pathlib import Path
from typing import Optional

import aiohttp
import safe_radix32
from clarabot_nano_common.common.config.constants import CommonConfigConstants


class WopiApiTester:
    def __init__(
        self,
        session: aiohttp.ClientSession,
        wopi_host_port: int,
        file_id: str,
        access_token: str,
        path: Path,
        read_only: bool,
        owner_id: int,
        user_id: int,
        user_data=None,
    ):
        self.session = session
        self.wopi_host_port = wopi_host_port
        self.file_id = file_id
        self.access_token = access_token
        self.path = path
        self.read_only = read_only
        self.owner_id = owner_id
        self.user_id = user_id
        self.user_friendly_name = user_data["avatar_name"] if user_data else None
        self.user_extra_info = (
            {
                "avatar": CommonConfigConstants.api_url(
                    f"r/avatar/{safe_radix32.encode_fw(user_id)}/{user_data['avatar_image_key']}"
                )
            }
            if user_data and user_data.get("avatar_image_key")
            else None
        )
        self.wopi_url_start = f"http://127.0.0.1:{self.wopi_host_port}/wopi/files/"

    def put_result(self):
        return {"LastModifiedTime": self.modification_timestamp(self.path.lstat())}

    def modification_timestamp(self, s: os.stat_result) -> str:
        return datetime.fromtimestamp(s.st_mtime).isoformat()

    def url(self, path: str = ""):
        return f"{self.wopi_url_start}{self.file_id}{path}?access_token={self.access_token}"

    async def check_file_info(self, error=None, template=None):
        async with self.session.get(self.url()) as resp:
            try:
                resp.raise_for_status()
            except aiohttp.ClientResponseError as e:
                if error is None:
                    raise e
                assert e.status == error, str(e)
            else:
                assert error is None, str(resp)
                result = await resp.json()
                s = self.path.lstat()
                ref = {
                    "BaseFileName": self.path.name,
                    "OwnerId": safe_radix32.encode_fw(self.owner_id),
                    "Size": s.st_size,
                    "UserId": safe_radix32.encode_fw(self.user_id),
                    "SupportsUpdate": True,
                    "SupportsGetLock": True,
                    "SupportsLocks": True,
                    "IsAnonymousUser": self.user_id == 0,  # NOTE: not about effective permission, just identity
                    "UserFriendlyName": self.user_friendly_name,
                    "ReadOnly": self.read_only,
                    "UserCanAttend": True,  # see update broadcast
                    "UserCanNotWriteRelative": self.read_only,
                    "UserCanPresent": not self.read_only,  # broadcast updates
                    "UserCanWrite": not self.read_only,
                    "FileExtension": self.path.suffix,
                    "LastModifiedTime": self.modification_timestamp(s),
                    "HidePrintOption": True,  # does not work yet
                    "EnableOwnerTermination": True,
                }
                if self.user_extra_info:
                    ref["UserExtraInfo"] = self.user_extra_info
                if template:
                    ref["TemplateSource"] = f"http://127.0.0.1:{self.wopi_host_port}/template/{template}"
                assert result == ref, f"x\n{result}\n{ref}"
                return result

    async def get_file(self, error=None):
        async with self.session.get(self.url("/contents")) as resp:
            try:
                resp.raise_for_status()
            except aiohttp.ClientResponseError as e:
                if error is None:
                    raise e
                assert e.status == error, str(e)
            else:
                assert error is None, str(resp)
                return await resp.read()

    async def get_lock(self, token: Optional[str] = "", status=200):
        async with self.session.post(self.url(), headers={"X-WOPI-Override": "GET_LOCK"}) as resp:
            assert resp.status == status, str(resp)
            lock = resp.headers.get("X-WOPI-Lock")
            if token is not None:
                assert lock == token, str(resp.headers)
            return lock

    async def lock(self, token: str, status=200, old_token: str = ""):
        headers = {"X-WOPI-Override": "LOCK", "X-WOPI-Lock": token}
        if old_token:
            headers["X-WOPI-OldLock"] = old_token
        async with self.session.post(self.url(), headers=headers) as resp:
            assert resp.status == status, str(resp)
            return resp.headers.get("X-WOPI-Lock")

    async def unlock(self, token: str, status=200):
        async with self.session.post(self.url(), headers={"X-WOPI-Override": "UNLOCK", "X-WOPI-Lock": token}) as resp:
            assert resp.status == status, str(resp)
            return resp.headers.get("X-WOPI-Lock")

    async def refresh_lock(self, token: str, status=200):
        async with self.session.post(
            self.url(), headers={"X-WOPI-Override": "REFRESH_LOCK", "X-WOPI-Lock": token}
        ) as resp:
            assert resp.status == status, str(resp)
            return resp.headers.get("X-WOPI-Lock")

    async def put_file(self, token: str, data: bytes, error=None, forced_ts: str = None):
        headers = {"X-WOPI-Override": "PUT", "X-WOPI-Lock": token}
        if forced_ts is not None:
            if forced_ts:
                headers["X-COOL-WOPI-Timestamp"] = forced_ts
        elif self.path.exists():
            headers["X-COOL-WOPI-Timestamp"] = self.modification_timestamp(self.path.lstat())
        async with self.session.post(self.url("/contents"), data=data, headers=headers) as resp:
            try:
                result = await resp.json()
            except Exception:
                result = None
            try:
                resp.raise_for_status()
            except aiohttp.ClientResponseError as e:
                if error is None:
                    raise e
                assert e.status == error, str(e)
            else:
                assert error is None, str(resp)
            return resp.headers.get("X-WOPI-Lock"), result

    async def put_relative_file(self, token: str, suggested: str, data: bytes, error=None, auto_names=False):
        async with self.session.post(
            self.url(),
            data=data,
            headers={
                "X-WOPI-Override": "PUT_RELATIVE",
                "X-WOPI-Lock": token,
                "X-WOPI-SuggestedTarget": suggested.encode("utf-7").decode("ascii"),
            },
        ) as resp:
            try:
                resp.raise_for_status()
            except aiohttp.ClientResponseError as e:
                if error is None:
                    raise e
                assert e.status == error, str(e)
                return resp.headers.get("X-WOPI-Lock")
            else:
                assert error is None, str(resp)
                result = await resp.json()
                assert set(result.keys()) == {"Name", "Url", "LastModifiedTime"}
                prf_p = self.path.parent / result["Name"]
                assert prf_p.read_bytes() == data
                if auto_names:
                    assert result["Name"] != suggested, str(result)
                    pre, _, post = suggested.rpartition(".")
                    assert result["Name"].startswith(pre), str(result)
                    assert result["Name"].endswith(post), str(result)
                else:
                    assert result["Name"] == suggested, str(result)
                assert result["Url"].startswith(self.wopi_url_start), str(result)
                assert result["Url"].find("?access_token=") > 0, str(result)
                assert result["LastModifiedTime"] == self.modification_timestamp(prf_p.lstat()), str(result)
                return result
