# -*- coding: utf-8 -*-

"""A wrapper around ROBOT functionality.

.. seealso:: https://robot.obolibrary.org
"""

import dataclasses
import json
import logging
import os
import subprocess
import tempfile
from contextlib import contextmanager
from dataclasses import dataclass
from pathlib import Path
from subprocess import check_output
from typing import List, Literal, Optional, Union

import bioregistry
import requests

from .obograph import Graph, GraphDocument

__all__ = [
    "ParseResults",
    "convert_to_obograph_local",
    "convert_to_obograph_remote",
    "convert_to_obograph",
    "get_obograph_by_prefix",
]

logger = logging.getLogger(__name__)


@dataclass
class ParseResults:
    """A dataclass containing an OBO Graph JSON and text output from ROBOT."""

    graph_document: GraphDocument
    messages: List[str] = dataclasses.field(default_factory=list)

    def squeeze(self) -> Graph:
        """Get the first graph."""
        return self.graph_document.graphs[0]


def get_obograph_by_prefix(
    prefix: str, *, json_path: Union[None, str, Path] = None
) -> ParseResults:
    """Get an ontology by its Bioregistry prefix."""
    if prefix != bioregistry.normalize_prefix(prefix):
        raise ValueError("this function requires bioregistry canonical prefixes")

    json_iri = bioregistry.get_json_download(prefix)

    if json_iri is not None:
        res_json = requests.get(json_iri).json()
        graph_document = GraphDocument(**res_json)
        return ParseResults(graph_document=graph_document)

    owl_iri = bioregistry.get_owl_download(prefix)
    obo_iri = bioregistry.get_obo_download(prefix)

    for iri in [owl_iri, obo_iri]:
        if iri is None:
            continue

        try:
            parse_results = convert_to_obograph_remote(iri, json_path=json_path)
        except subprocess.CalledProcessError:
            logger.info("could not parse OBO for %s from %s", prefix, iri)
            continue
        else:
            return parse_results

    raise RuntimeError(f"no IRI available for Bioregistry prefix {prefix}")


def convert_to_obograph_local(
    path: Union[str, Path],
    *,
    json_path: Union[None, str, Path] = None,
) -> ParseResults:
    """Convert a local OWL/OBO file to an OBO Graph JSON object.

    :param path: The path to a local OWL or OBO file
    :param json_path: The optional path to store the intermediate
        OBO Graph JSON file generated by ROBOT. If not given, the
        OBO Graph JSON file will be put in a temporary directory
        and deleted after the function finishes.
    :returns: An object with the parsed OBO Graph JSON and text
        output from the ROBOT conversion program
    """
    return convert_to_obograph(input_str=path, flag="-i", json_path=json_path)


def convert_to_obograph_remote(
    iri: str,
    *,
    json_path: Union[None, str, Path] = None,
) -> ParseResults:
    """Convert a remote OWL/OBO file to an OBO Graph JSON object.

    :param iri: The IRI for a remote OWL or OBO file
    :param json_path: The optional path to store the intermediate
        OBO Graph JSON file generated by ROBOT. If not given, the
        OBO Graph JSON file will be put in a temporary directory
        and deleted after the function finishes.
    :returns: An object with the parsed OBO Graph JSON and text
        output from the ROBOT conversion program
    """
    return convert_to_obograph(input_str=iri, flag="-I", json_path=json_path)


def convert_to_obograph(
    input_str: Union[str, Path],
    *,
    flag: Optional[Literal["-i", "-I"]] = None,
    json_path: Union[None, str, Path] = None,
) -> ParseResults:
    """Convert a local OWL file to a JSON file.

    :param input_str: Either a local file path or IRI. If a local file path
        is used, pass ``"-i"`` to ``flag``. If an IRI is used, pass ``"-I"``
        to ``flag``.
    :param flag: The flag to denote if the file is local or remote.
        Tries to infer from input string if none is given
    :param json_path: The optional path to store the intermediate
        OBO Graph JSON file generated by ROBOT. If not given, the
        OBO Graph JSON file will be put in a temporary directory
        and deleted after the function finishes.
    :returns: An object with the parsed OBO Graph JSON and text
        output from the ROBOT conversion program
    """
    if flag is None:
        flag = "-I" if _is_remote(input_str) else "-i"
    with _path_context(json_path) as path:
        args = ["robot", "convert", flag, str(input_str), "-o", str(path), "--format", "json"]
        ret = check_output(args, cwd=os.path.dirname(__file__))  # noqa:S603
        messages = ret.decode().strip().splitlines()
        graph_document_raw = json.loads(path.read_text())
        for i, graph in enumerate(graph_document_raw["graphs"]):
            if "id" not in graph:
                raise ValueError(f"{input_str} graph [{i}] missing id")
        graph_document = GraphDocument(**graph_document_raw)
        return ParseResults(graph_document=graph_document, messages=messages)


#: Prefixes that denote remote resources
PROTOCOLS = {
    "https://",
    "http://",
    "ftp://",
    "ftps://",
}


def _is_remote(url: Union[str, Path]) -> bool:
    return isinstance(url, str) and any(url.startswith(protocol) for protocol in PROTOCOLS)


@contextmanager
def _path_context(path: Union[None, str, Path], name: str = "output.json"):
    if path is not None:
        yield Path(path).resolve()
    else:
        with tempfile.TemporaryDirectory() as directory:
            yield Path(directory).joinpath(name)
