#!/usr/bin/env python3
import asyncio
import functools
import json
import logging
import os.path
import sys
from typing import Optional

import fire
import humanfriendly
from termcolor import colored

from .client import StcGeck
from .exceptions import IpfsConnectionError


def exception_handler(func):
    @functools.wraps(func)
    async def wrapper_func(*args, **kwargs):
        try:
            await func(*args, **kwargs)
        except IpfsConnectionError as e:
            print(
                f"{colored('INFO', 'red')}: Cannot connect to IPFS: {e.info}\n"
                f"Try to pass working IPFS address with `--ipfs-http-base-url` parameter",
                file=sys.stderr,
            )
    return wrapper_func


class StcGeckCli:
    def __init__(
        self,
        ipfs_http_base_url: str,
        ipfs_api_base_url: str,
        ipfs_data_directory: str,
        grpc_api_endpoint: str,
        index_alias: str,
        timeout: int,
        profile: str,
    ):
        self.geck = StcGeck(
            ipfs_http_base_url=ipfs_http_base_url,
            ipfs_api_base_url=ipfs_api_base_url,
            ipfs_data_directory=ipfs_data_directory,
            grpc_api_endpoint=grpc_api_endpoint,
            index_alias=index_alias,
            timeout=timeout,
            profile=profile,
        )
        self.grpc_api_endpoint = grpc_api_endpoint
        self.index_alias = index_alias

    @exception_handler
    async def documents(self):
        """
        Stream all STC chunks.

        :return: metadata records
        """
        print(f"{colored('INFO', 'green')}: Setting up indices...")
        async with self.geck as geck:
            async for document in geck.get_summa_client().documents(self.index_alias):
                print(document)

    @exception_handler
    async def download(self, query: str, output_path: str):
        """
        Download file from STC using default Summa match queries.
        Examples: `doi:10.1234/abc, isbns:9781234567890`

        :param query: query in Summa match format
        :param output_path: filepath for writing file

        :return: file if record has corresponding CID
        """
        results = await self.search(query)
        output_path, output_path_ext = os.path.splitext(output_path)
        output_path_ext = output_path_ext.lstrip('.')
        if results:
            print(f"{colored('INFO', 'green')}: Found {query}")
            if 'cid' in results[0]:
                print(f"{colored('INFO', 'green')}: Receiving file {query}...")
                if (real_extension := results[0].get('extension', 'pdf')) != output_path_ext:
                    print(
                        f"{colored('WARN', 'yellow')}: Receiving file extension `{real_extension}` "
                        f"is not matching with your output path extension `{output_path_ext}`. Changed to correct one.")
                    output_path_ext = real_extension
                data = await self.geck.download(results[0]["cid"])
                final_file_name = output_path + '.' + output_path_ext
                with open(final_file_name, 'wb') as f:
                    f.write(data)
                    f.close()
                    print(f"{colored('INFO', 'green')}: File {final_file_name} is written")
            else:
                print(f"{colored('ERROR', 'red')}: Not found CID for {query}", file=sys.stderr)
        else:
            print(f"{colored('ERROR', 'red')}: Not found {query}", file=sys.stderr)

    @exception_handler
    async def random_cids(self, n: Optional[int] = None, space: Optional[str] = None):
        """
        Return random CIDs for pinning from the STC Hub API.

        :param n: number of items to pin
        :param space: approximate disk space you would like to allocate for pinning
        """
        if not n and not space:
            raise ValueError("`n` or `space_bytes` should be set")
        if space:
            # 3.61MB is the average size of the item
            n = int(humanfriendly.parse_size(space) / (3.61 * 1024 * 1024))
        async with self.geck as geck:
            return await geck.random_cids(n=n)

    async def search(self, query: str, limit: int = 1):
        """
        Searches in STC using default Summa match queries.
        Examples: `doi:10.1234/abc, isbns:9781234567890, "fetal hemoglobin"`

        :param query: query in Summa match format
        :param limit: how many results to return, higher values incurs LARGE performance penalty.

        :return: metadata records
        """
        print(f"{colored('INFO', 'green')}: Setting up indices...")
        async with self.geck as geck:
            print(f"{colored('INFO', 'green')}: Searching {query}...")
            query_processor = geck.get_query_processor()
            processed_query = query_processor.process(query, limit=limit)
            logging.getLogger('statbox').info({'action': 'search', 'processed_query': processed_query})
            summa_client = geck.get_summa_client()
            response = await summa_client.search(processed_query)
            documents = list(map(lambda x: json.loads(x.document), response.collector_outputs[0].documents.scored_documents))
            return documents

    async def serve(self):
        """
        Start serving Summa
        """
        print(f"{colored('INFO', 'green')}: Setting up indices...")
        async with self.geck:
            print(f"{colored('INFO', 'green')}: Serving on {self.grpc_api_endpoint}")
            while True:
                await asyncio.sleep(5)

    @exception_handler
    async def create_ipfs_directory(
        self,
        output_car: str,
        query: str = None,
        limit: int = 100,
        name_template: str = '{id}.{extension}',
    ):
        """
        Stream all STC chunks.

        :return: metadata records
        """
        print(f"{colored('INFO', 'green')}: Setting up indices...")
        async with self.geck as geck:
            return await geck.create_ipfs_directory(output_car, query, limit, name_template)


async def stc_geck_cli(
    ipfs_http_base_url: str = 'http://127.0.0.1:8080',
    ipfs_api_base_url: str = 'http://127.0.0.1:5001',
    ipfs_data_directory: str = '/ipns/standard-template-construct.org/data',
    grpc_api_endpoint: str = '127.0.0.1:10082',
    index_alias: str = 'nexus_science',
    profile: str = 'light',
    timeout: int = 120,
    debug: bool = False,
):
    """
    :param ipfs_http_base_url: IPFS HTTP Endpoint
    :param ipfs_api_base_url: IPFS HTTP API Endpoint
    :param ipfs_data_directory: path to the directory with index
    :param grpc_api_endpoint: port used for Summa
    :param index_alias: default index alias
    :param profile: query processor profile
    :param timeout: timeout for requests to IPFS
    :param debug: add debugging output
    :return:
    """
    logging.basicConfig(stream=sys.stdout, level=logging.INFO if debug else logging.ERROR)
    stc_geck_client = StcGeckCli(
        ipfs_http_base_url=ipfs_http_base_url,
        ipfs_api_base_url=ipfs_api_base_url,
        ipfs_data_directory=ipfs_data_directory,
        grpc_api_endpoint=grpc_api_endpoint,
        index_alias=index_alias,
        timeout=timeout,
        profile=profile,
    )
    return {
        'create-ipfs-directory': stc_geck_client.create_ipfs_directory,
        'documents': stc_geck_client.documents,
        'download': stc_geck_client.download,
        'random-cids': stc_geck_client.random_cids,
        'search': stc_geck_client.search,
        'serve': stc_geck_client.serve,
    }


def main():
    fire.Fire(stc_geck_cli, name='geck')


if __name__ == '__main__':
    main()
