import asyncio
import os

import aiohttp
from typing import Optional, BinaryIO, AsyncIterator, Union

from aiohttp import payload
from orjson import orjson

from filedgr_lib_ipfs.my_io.my_file_io import build_dir_tree

SyncReadable = BinaryIO  # works for botocore.response.StreamingBody
AsyncReadable = AsyncIterator[bytes]
Readable = Union[SyncReadable, AsyncReadable]


class IpfsClient:

    def __init__(self, host: str, port: int, protocol: str = "http", **kwargs):
        if host is None and port is None and 'host' not in kwargs and 'port' not in kwargs:
            raise AttributeError("To initialize the IPFS client a hostname/ip and port are required.")
        else:
            self.__host = host
            self.__port = port
            self.__protocol = protocol

    def __get_addr(self):
        return f"{self.__protocol}://{self.__host}:{self.__port}"

    async def ls(self):
        async with aiohttp.ClientSession() as session:
            url = f"{self.__get_addr()}/api/v0/files/ls"
            async with session.post(url=url) as response:
                if response.status == 200:
                    return await response.json()

    async def add_stream(self,
                         stream: Readable,
                         *,
                         filename: str,
                         raw_leaves: bool = True,
                         chunk_size: int = 8 * 1024 * 1024) -> str:
        url = (
            f"{self.__get_addr()}/api/v0/add"
            f"?cid-version=1"
            f"&hash=sha2-256"
            f"&chunker=size-262144"
            f"&raw-leaves=true"
            f"&trickle=false"
            f"&wrap-with-directory=false"
            f"&pin=false"
            f"&progress=false"
            # f"&raw-leaves={'true' if raw_leaves else 'false'}"
        )

        timeout = aiohttp.ClientTimeout(
            total=None,  # large files: no global cap
            sock_connect=30,
            sock_read=None  # allow long processing
        )

        connector = aiohttp.TCPConnector(
            limit=20,
            limit_per_host=10,
            enable_cleanup_closed=True
        )

        async_iter = self._to_async_iter(stream, chunk_size=chunk_size)

        form = aiohttp.FormData()
        form.add_field(
            name="file",
            value=payload.AsyncIterablePayload(
                async_iter,
                content_type="application/octet-stream",
            ),
            filename=filename,
        )

        async with aiohttp.ClientSession(timeout=timeout, connector=connector) as session:
            async with session.post(url, data=form) as resp:
                if resp.status != 200:
                    txt = await resp.text()
                    raise RuntimeError(f"IPFS add failed ({resp.status}): {txt[:1500]}")

                # kubo returns NDJSON; final object includes "Hash"
                last: Optional[dict] = None
                async for raw_line in resp.content:
                    line = raw_line.decode("utf-8", errors="replace").strip()
                    if not line:
                        continue
                    last = orjson.loads(line)

                if not last or "Hash" not in last:
                    raise RuntimeError("IPFS add returned no final Hash/CID.")

                return last["Hash"]

    async def add_file(self,
                       path: str) -> str:
        if os.path.exists(path) and os.path.isfile(path):
            with open(path, "rb") as file:
                cid = await self.add_stream(file, filename=os.path.basename(path))
                return cid
        else:
            raise FileNotFoundError(f"The file: {path} was not found.")

    async def add_directory(self,
                            path: str,
                            remove_prefix: Optional[str] = None) -> str:
        dirs, files = build_dir_tree(path)
        params = {'pin': 'true'}
        async with aiohttp.ClientSession() as session:
            url = f"{self.__get_addr()}/api/v0/add?cid-version=1"
            with aiohttp.MultipartWriter('form-data') as mpwriter:

                for dir in dirs:
                    if remove_prefix:
                        name = dir.removeprefix(remove_prefix)
                        print(f"removed prefix:{remove_prefix} for dir: {name}")
                    else:
                        name = dir
                        print(f"dir: {name}")
                    folder_part = mpwriter.append(obj='', headers={'content-type': 'application/x-directory'})
                    folder_part.set_content_disposition('form-data', name="file", filename=name)

                for file in files:
                    if remove_prefix:
                        name = file.removeprefix(remove_prefix)
                        print(f"removed prefix:{remove_prefix} for file: {name}")
                    else:
                        name = file
                        print(f"file: {name}")
                    file_part = mpwriter.append(open(file, "rb"))
                    file_part.set_content_disposition('form-data', name="file", filename=name)

                print(f"Calling url: {url}")
                async with session.post(url=url, data=mpwriter, params=params) as resp:
                    if resp.status == 200:
                        import json
                        res = await resp.text()
                        res = res.replace("\n", "")
                        res = res.replace("}{", "},{")
                        res = f"[{res}]"
                        res_dict = json.loads(res)
                        return res_dict
                    else:
                        print(await resp.text())

    def _to_async_iter(self, stream: Readable, *, chunk_size: int) -> AsyncIterator[bytes]:
        # If it's already async, return as-is.
        if hasattr(stream, "__aiter__"):
            return stream

        # Otherwise treat as sync file-like and read it in a threadpool to avoid blocking the event loop.
        return self._sync_readable_to_async_iter(stream, chunk_size=chunk_size)

    async def _sync_readable_to_async_iter(self, stream: SyncReadable, *, chunk_size: int) -> AsyncIterator[bytes]:
        loop = asyncio.get_running_loop()
        while True:
            chunk = await loop.run_in_executor(None, stream.read, chunk_size)
            if not chunk:
                break
            yield chunk
