import json
import re
from collections import OrderedDict
from importlib import metadata
from typing import Union, Tuple
from urllib.parse import urljoin, urlparse

from requests import Session
from requests.auth import HTTPBasicAuth

try:
    from importlib.resources import files
except ImportError:
    from importlib_resources import files

OAS_DIR = files("mini_ipfabric.oas")
RE_PATH = re.compile(r"^/?(api/)?v\d(\.\d)?/")


class IPFClient(Session):
    def __init__(
        self,
        base_url: str,
        auth: Union[str, Tuple[str, str]],
        snapshot_id: str = "$last",
        verify: bool = True,
        timeout: float = None,
    ):
        super().__init__()
        self.verify = verify
        self.base_url = base_url
        self.timeout = timeout
        self.headers["User-Agent"] = (
            f'mini-ipfabric-sdk/{metadata.version("mini_ipfabric")}'
        )
        if isinstance(auth, str):
            self.headers["X-API-Token"] = auth
        elif isinstance(auth, tuple):
            self.auth = HTTPBasicAuth(auth[0], auth[1])
        else:
            self.auth = auth

        resp = self.get(urljoin(self.base_url, "/api/version"))
        resp.raise_for_status()
        self.version = resp.json()["apiVersion"]
        self.oas = self._load_oas()
        self.base_url = urljoin(self.base_url, f"/api/{self.version}/")
        self.snapshots = self.get_loaded_snapshots()
        self._snapshot_id, self.snapshot = None, None
        self.snapshot_id = snapshot_id

    def _load_oas(self):
        try:
            return json.loads(OAS_DIR.joinpath(self.version + ".json").read_text())
        except FileNotFoundError:
            paths = self.get(urljoin(self.base_url, "/api/oas/openapi.json")).json()[
                "paths"
            ]
            oas = dict()
            for path, methods in paths.items():
                oas[path[1:]] = {"api_endpoint": path}
                for method in ["get", "post"]:
                    if method not in methods:
                        oas[path[1:]][method] = None
                        continue
                    spec = methods[method]
                    try:
                        web_endpoint = spec["x-table"]["webPath"]
                    except KeyError:
                        web_endpoint = None
                    try:
                        columns = list(
                            set(
                                spec["requestBody"]["content"]["application/json"][
                                    "schema"
                                ]["properties"]["columns"]["items"]["enum"]
                            )
                        )
                    except KeyError:
                        columns = None
                    oas[path[1:]][method] = {
                        "api_endpoint": path[1:],
                        "summary": spec["summary"],
                        "description": spec["description"],
                        "web_endpoint": web_endpoint,
                        "columns": columns,
                    }
            return oas

    @property
    def snapshot_id(self):
        return self._snapshot_id

    @snapshot_id.setter
    def snapshot_id(self, v):
        self.snapshot = self.snapshots[v]
        self._snapshot_id = self.snapshot["id"]

    @property
    def web_path(self):
        return {
            v["post"]["web_endpoint"]: v["post"]
            for v in self.oas.values()
            if v["post"] and v["post"]["web_endpoint"]
        }

    def _check_url(self, url) -> str:
        path = urlparse(url).path
        path = path if path[0] == "/" else "/" + path
        if path in self.web_path:
            return self.web_path[path]['api_endpoint']
        r = RE_PATH.search(path)
        url = path[r.end():] if r else path  # fmt: skip
        url = url[1:] if url[0] == "/" else url
        return url

    @property
    def technology(self):
        return {k: v for k, v in self.web_path.items() if k.startswith("/technology")}

    @property
    def inventory(self):
        return {k: v for k, v in self.web_path.items() if k.startswith("/inventory")}

    def fetch_all(self, endpoint: str, reports: bool = False, filters: dict = None, columns: list = None):
        path = self._check_url(endpoint)
        data = self.oas[path]['post']
        payload = {
            "columns": columns or data["columns"],
            "snapshot": self.snapshot_id,
        }
        if reports:
            payload["reports"] = data['web_endpoint']
        if filters:
            payload["filters"] = filters
        return self._ipf_pager(
            urljoin(self.base_url, data["api_endpoint"]), payload=payload
        )

    def get_intents(self):
        resp = self.get(
            urljoin(self.base_url, "reports"), params=dict(snapshot=self.snapshot_id)
        )
        resp.raise_for_status()
        intents = resp.json()
        for intent in intents:
            for _ in ["0", "10", "20", "30"]:
                if _ not in intent["result"]["checks"]:
                    intent["result"]["checks"][_] = None
        return intents

    def get_loaded_snapshots(self):
        payload = {
            "columns": self.oas["tables/management/snapshots"]["post"]["columns"],
            "sort": {"order": "desc", "column": "tsEnd"},
            "filters": {
                "and": [{"status": ["eq", "done"]}, {"finishStatus": ["eq", "done"]}]
            },
        }
        resp = self.post(
            urljoin(self.base_url, "tables/management/snapshots"),
            json=payload,
            timeout=self.timeout,
        )
        resp.raise_for_status()
        snap_dict = OrderedDict()
        for snap in resp.json()["data"]:
            snap_dict[snap["id"]] = snap
            if "$lastLocked" not in snap_dict and snap["locked"]:
                snap_dict["$lastLocked"] = snap
            if "$last" not in snap_dict:
                snap_dict["$last"] = snap
                continue
            if "$prev" not in snap_dict:
                snap_dict["$prev"] = snap
        return snap_dict

    def _ipf_pager(
        self,
        url: str,
        payload: dict,
        limit: int = 1000,
        start: int = 0,
    ):
        """
        Loops through and collects all the data from the tables
        :param url: str: Full URL to post to
        :param payload: dict: Data to submit to IP Fabric
        :param start: int: Where to start for the data
        :return: list: List of dictionaries
        """
        payload["pagination"] = dict(limit=limit)
        data = list()

        def page(s):
            payload["pagination"]["start"] = s
            r = self.post(url, json=payload, timeout=self.timeout)
            r.raise_for_status()
            return r.json()["data"]

        r_data = page(start)
        data.extend(r_data)
        while limit == len(r_data):
            start = start + limit
            r_data = page(start)
            data.extend(r_data)
        return data
