from __future__ import annotations

import asyncio
from contextlib import asynccontextmanager
from typing import Any, Callable, NamedTuple, cast

from qiniu import Auth, BucketManager, build_batch_delete, etag, put_data, put_file
from qiniu.http import ResponseInfo

from . import bhttp, bpath, bzip
from .bfunc import json_dumps_mini
from .btype import XPath


class QiniuBucket:
    def __init__(self, bucket: str, base_url: str, ak: str, sk: str) -> None:
        self.q = Auth(ak, sk)
        self.bucket_manager = BucketManager(self.q)
        self.bucket = bucket
        self.base_url = base_url

    async def upload_bytes(self, key: str, data: bytes):
        token = self.q.upload_token(self.bucket, key)
        info: ResponseInfo
        _, info = await asyncio.to_thread(put_data, token, key, data)
        assert info.exception is None
        assert info.status_code == 200
        return self.get_public_file_url(key)

    async def upload_str(self, key: str, data: str):
        await self.upload_bytes(key, data.encode())
        return self.get_public_file_url(key)

    async def upload_json(self, key: str, data: dict[str, Any]):
        await self.upload_str(key, json_dumps_mini(data))
        return self.get_public_file_url(key)

    async def get_private_bytes(self, key: str) -> bytes:
        url = self.get_private_file_url(key)
        return await bhttp.get_bytes(url)

    async def get_private_str(self, key: str) -> str:
        url = self.get_private_file_url(key)
        return await bhttp.get_str(url)

    async def get_private_json(self, key: str) -> dict[str, Any]:
        url = self.get_private_file_url(key)
        return await bhttp.get_json(url)

    async def get_public_bytes(self, key: str) -> bytes:
        return await bhttp.get_bytes(self.get_public_file_url(key))

    async def get_public_str(self, key: str) -> str:
        return await bhttp.get_str(self.get_public_file_url(key))

    async def get_public_json(self, key: str) -> dict[str, Any]:
        return await bhttp.get_json(self.get_public_file_url(key))

    async def upload_file(self, key: str, local_file: XPath):
        token = self.q.upload_token(self.bucket, key)
        info: ResponseInfo
        _, info = await asyncio.to_thread(put_file, token, key, local_file, version='v2')
        assert info.exception is None
        assert info.status_code == 200
        return self.get_public_file_url(key)

    def get_public_file_url(self, key: str):
        return f'{self.base_url}/{key}'

    def get_private_file_url(self, key: str):
        return self.q.private_download_url(self.get_public_file_url(key))

    @asynccontextmanager
    async def _download_private_file(self, key: str):
        url = self.get_private_file_url(key)
        with bpath.use_temp_file() as temp_file:
            temp_file = bpath.temp_file()
            await bhttp.download(url, temp_file)
            assert temp_file.exists()
            yield temp_file

    async def download_private_file(self, key: str, local_file: XPath):
        async with self._download_private_file(key) as temp_file:
            bpath.move(temp_file, local_file, True)

    async def download_private_file_unzip(self, key: str, output_dir: XPath):
        async with self._download_private_file(key) as temp_file:
            with bpath.use_temp_path() as temp_dir:
                bzip.unzip(temp_file, temp_dir)
                for file in bpath.list_file(temp_dir, True):
                    to_file = bpath.change_relative(file, temp_dir, output_dir)
                    bpath.move(file, to_file, True)

    async def download_private_file_seven_unzip(self, key: str, output_dir: XPath):
        async with self._download_private_file(key) as temp_file:
            with bpath.use_temp_path() as temp_dir:
                await bzip.seven_unzip(temp_file, temp_dir)
                for file in bpath.list_file(temp_dir, True):
                    to_file = bpath.change_relative(file, temp_dir, output_dir)
                    bpath.move(file, to_file, True)

    async def get_file_list(self, prefix: str, limit: int = 100) -> tuple[list[QiniuItem], str | None]:
        result: _FileListResult
        result, _, _ = await asyncio.to_thread(self.bucket_manager.list, self.bucket, prefix, None, limit)
        assert type(result) is dict
        file_list = [QiniuItem(x['key'], x['fsize'], x['hash'], x['putTime']) for x in result['items']]
        return file_list, cast(str | None, result.get('marker', None))

    async def get_file_list_by_marker(self, marker: str, limit: int = 100):
        result: _FileListResult
        result, _, _ = await asyncio.to_thread(self.bucket_manager.list, self.bucket, None, marker, limit)
        assert type(result) is dict
        file_list = [QiniuItem(x['key'], x['fsize'], x['hash'], x['putTime']) for x in result['items']]
        return file_list, cast(str | None, result.get('marker', None))

    async def delete_files(self, *key_list: str):
        result, _ = await _run(self.bucket_manager.batch, build_batch_delete(self.bucket, list(key_list)))
        assert result

    async def hash_file(self, file: XPath):
        return await asyncio.to_thread(etag, file)

    async def get_file_status(self, key: str):
        result, _ = await _run(self.bucket_manager.stat, self.bucket, key)
        return result

    async def move(self, old_key: str, new_key: str):
        result, _ = await _run(self.bucket_manager.move, self.bucket, old_key, self.bucket, new_key)
        assert result == {}
        return self.get_public_file_url(new_key)

    async def copy(self, old_key: str, new_key: str):
        result, _ = await _run(self.bucket_manager.copy, self.bucket, old_key, self.bucket, new_key)
        assert result == {}
        return self.get_public_file_url(new_key)


# ------------------------------------------------------------------------------------


class QiniuItem(NamedTuple):
    key: str
    size: int
    qetag: str
    time: int


_FileListResult = tuple[dict[str, Any], Any, Any]


async def _run(func: Callable[..., Any], *args: Any, **kwargs: Any) -> tuple[Any, Any]:
    return await asyncio.to_thread(func, *args, **kwargs)
