# -*- 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, Optional, Union

import bioregistry
import requests
from typing_extensions import Literal

from .obograph import Graph, GraphDocument

__all__ = [
    "is_available",
    "ParseResults",
    # Conversions
    "convert",
    "convert_to_obograph_local",
    "convert_to_obograph_remote",
    "convert_to_obograph",
    # Processors
    "get_obograph_by_prefix",
    "get_obograph_by_iri",
]

logger = logging.getLogger(__name__)


def is_available() -> bool:
    """Check if ROBOT is available."""
    # suggested in https://stackoverflow.com/questions/11210104/check-if-a-program-exists-from-a-python-script
    from shutil import which

    return which("robot") is not None


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

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

    def squeeze(self) -> Graph:
        """Get the first graph."""
        if self.graph_document is None:
            raise ValueError(f"graph document was not successfully parsed: {self.messages}")
        return self.graph_document.graphs[0]


def get_obograph_by_iri(
    iri: str,
) -> ParseResults:
    """Get an ontology by its OBO Graph JSON iri."""
    res_json = requests.get(iri).json()
    graph_document = GraphDocument(**res_json)
    return ParseResults(graph_document=graph_document, iri=iri)


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")

    messages = []
    json_iri = bioregistry.get_json_download(prefix)

    if json_iri is not None:
        try:
            return get_obograph_by_iri(json_iri)
        except (IOError, ValueError):
            msg = f"could not parse JSON for {prefix} from {json_iri}"
            messages.append(msg)
            logger.warning(msg)

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

    for label, iri in [("OWL", owl_iri), ("OBO", obo_iri)]:
        if iri is None:
            continue

        try:
            parse_results = convert_to_obograph_remote(iri, json_path=json_path)
        except subprocess.CalledProcessError:
            msg = f"could not parse {label} for {prefix} from {iri}"
            messages.append(msg)
            logger.warning(msg)
            continue
        else:
            # stick all messages before
            parse_results.messages = [*messages, *parse_results.messages]
            return parse_results

    return ParseResults(graph_document=None, messages=messages)


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_path=path, input_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_path=iri, input_flag="-I", json_path=json_path, input_is_iri=True
    )


def convert_to_obograph(
    input_path: Union[str, Path],
    *,
    input_flag: Optional[Literal["-i", "-I"]] = None,
    json_path: Union[None, str, Path] = None,
    input_is_iri: bool = False,
    extra_args: Optional[List[str]] = None,
) -> ParseResults:
    """Convert a local OWL file to a JSON file.

    :param input_path: 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 input_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.
    :param input_is_iri:
        Should the ``input_path`` varible be considered as an IRI that
        gets stored in the returned parse results?
    :param extra_args:
        Extra positional arguments to pass in the command line

    :returns: An object with the parsed OBO Graph JSON and text
        output from the ROBOT conversion program

    :raises ValueError: if a graph is missing an ID
    :raises TypeError: if ``input_as_iri`` is marked as true but a path
        object is given for the ``input_path``
    """
    if input_is_iri and not isinstance(input_path, str):
        raise TypeError
    with _path_context(json_path) as path:
        ret = convert(
            input_path=input_path,
            input_flag=input_flag,
            output_path=path,
            fmt="json",
            extra_args=extra_args,
        )
        messages = ret.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_path} graph [{i}] missing id")
        graph_document = GraphDocument(**graph_document_raw)
        return ParseResults(
            graph_document=graph_document,
            messages=messages,
            iri=input_path if input_is_iri else None,  # type:ignore
        )


#: 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)


def convert(
    input_path: Union[str, Path],
    output_path: Union[str, Path],
    input_flag: Optional[Literal["-i", "-I"]] = None,
    *,
    fmt: Optional[str] = None,
    extra_args: Optional[List[str]] = None,
) -> str:
    """Convert an OBO file to an OWL file with ROBOT."""
    if input_flag is None:
        input_flag = "-I" if _is_remote(input_path) else "-i"
    args = [
        "robot",
        "convert",
        input_flag,
        str(input_path),
        "-o",
        str(output_path),
        *(extra_args or []),
    ]
    if fmt:
        args.extend(("--format", fmt))
    logger.debug("Running shell command: %s", args)
    ret = check_output(  # noqa:S603
        args,
        cwd=os.path.dirname(__file__),
    )
    return ret.decode()
