import logging
from dataclasses import InitVar, field
from functools import cached_property
from pathlib import Path
from typing import Dict
from typing import Optional, Any, Union, Literal, List, Mapping, OrderedDict, Iterator

from dotenv import find_dotenv
from httpx import Client, BaseTransport, URL, Response
from httpx._client import EventHook
from httpx._types import CertTypes, VerifyTypes
from pydantic import ConfigDict, Field, InstanceOf, FilePath
from pydantic.dataclasses import dataclass
from pydantic_settings.sources import DotenvType

from ipfabric.auth import Setup, ProxyTypes, TimeoutTypes, ProxiesTypes
from ipfabric.exceptions import api_insuf_rights
from ipfabric.models import Snapshot, OAS, Endpoint, Snapshots, Methods, User
from ipfabric.tools import VALID_REFS, trigger_backup, raise_for_status

logger = logging.getLogger("ipfabric")

LAST_ID, PREV_ID, LASTLOCKED_ID = VALID_REFS


@dataclass(config=ConfigDict(arbitrary_types_allowed=True))
class IPFabricAPI:
    """Initializes the IP Fabric Client.

    Args:
        base_url: IP Fabric instance provided in 'base_url' parameter, or the 'IPF_URL' environment variable
        api_version: [Optional] Version of IP Fabric API
        snapshot_id: IP Fabric snapshot ID to use by default for database actions - defaults to '$last'
        auth: API token, tuple (username, password), or custom Auth to pass to httpx
        unloaded: True to load metadata from unloaded snapshots
        env_file: Path to .env file to load
        streaming: Default True to use streaming instead of paging.
        verify: httpx.Client - Default True to verify IPF SSL certificate.
        timeout: httpx.Client - Default 5
        proxy: httpx.Client
        proxies: httpx.Client
        mounts: httpx.Client
        cert: httpx.Client
        event_hooks: httpx.Client
        local_oas: Default True, False mainly for development purposes
        local_oas_file: Default True, False mainly for development purposes
        debug: Future use.
    """

    base_url: Optional[Union[str, InstanceOf[URL]]] = Field(
        None, description="Base URL of the IPF instance", examples=["https://demo.ipfabric.io"]
    )
    api_version: Optional[str] = Field(None, description="Defaults to SDK or API version.")
    snapshot_id: Optional[str] = Field(None, description="Defaults to '$last'.")
    auth: InitVar[Optional[Any]] = field(default=None)
    unloaded: bool = Field(False, description="Default False, do not load unloaded snapshot metadata.")
    env_file: InitVar[Optional[DotenvType]] = field(default=None)
    streaming: bool = Field(True, description="Default True; use streaming results instead of pagination.")
    verify: InitVar[Optional[VerifyTypes]] = field(default=None)
    timeout: InitVar[Optional[Union[TimeoutTypes, Literal["DEFAULT"]]]] = field(default="DEFAULT")
    proxy: InitVar[Optional[ProxyTypes]] = field(default=None)
    proxies: InitVar[Optional[ProxiesTypes]] = field(default=None)  # TODO: 7.0 this is deprecated.
    mounts: InitVar[Optional[Mapping[str, Optional[BaseTransport]]]] = field(default=None)
    cert: InitVar[Optional[CertTypes]] = field(default=None)
    event_hooks: InitVar[Optional[Mapping[str, List[EventHook]]]] = field(default=None)
    local_oas: bool = Field(True, description="Default True, use local minified OAS file instead of servers.")
    local_oas_file: Optional[FilePath] = None
    debug: Optional[bool] = None
    http2: InitVar[Optional[bool]] = field(default=True)
    _os_version: Optional[str] = None
    _os_api_version: Optional[str] = None
    _client: Client = None
    _prev_snapshot_id: Optional[str] = None
    _attribute_filters: Optional[dict] = None
    _no_loaded_snapshots: bool = False
    _oas: Optional[OAS] = None
    _user: Optional[User] = None
    _snapshots: Optional[Snapshots] = None

    def __post_init__(
        self,
        auth,
        env_file,
        verify,
        timeout,
        proxy,
        proxies,
        mounts,
        cert,
        event_hooks,
        http2,
    ):
        env_file = env_file if env_file else find_dotenv(usecwd=True) or Path("~").expanduser().joinpath(".env")
        logger.info(f"Using .env file located at {env_file}.")
        setup = Setup(
            base_url=self.base_url,
            api_version=self.api_version,
            auth=auth,
            _env_file=env_file,
            snapshot_id=self.snapshot_id,
            verify=verify,
            timeout=timeout,
            proxy=proxy,
            proxies=proxies,
            mounts=mounts,
            cert=cert,
            event_hooks=event_hooks,
            debug=self.debug,
            http2=http2,
        )
        [setattr(self, k, v) for k, v in setup.update_attrs.items()]

        self._oas = OAS(client=self, local_oas=self.local_oas, local_oas_file=self.local_oas_file)
        self._user = self.get_user()
        self._snapshots = Snapshots(client=self)
        self.snapshot_id = setup.snapshot_id
        logger.debug(
            f"Successfully connected to '{self.base_url.host}' IPF version '{self.os_version}' "
            f"as user '{self.user.username}'"
        )

    def __setattr__(self, name: str, value: Any) -> None:
        if name == "timeout":
            self._client.timeout = value
        elif name == "snapshot_id":
            value = self._switch_snapshot(value)
        super().__setattr__(name, value)

    def _switch_snapshot(self, snapshot_id: str) -> Union[str, None]:
        """Verify snapshot ID is valid."""
        if not self.loaded_snapshots:
            logger.warning("No Snapshots are currently loaded.  Please load a snapshot before querying any data.")
            self._no_loaded_snapshots = True
            return None
        elif snapshot_id in self.snapshots:
            self._prev_snapshot_id = self.snapshot_id
            return self.snapshots[snapshot_id].snapshot_id
        else:
            raise ValueError(f"Incorrect Snapshot ID: '{snapshot_id}'")

    def get(self, url, *args, params=None, **kwargs) -> Response:
        return self._client.get(url, *args, params=params, **kwargs)

    def post(self, url, *args, json=None, **kwargs) -> Response:
        return self._client.post(url, *args, json=json, **kwargs)

    def put(self, url, *args, json=None, **kwargs) -> Response:
        return self._client.put(url, *args, json=json, **kwargs)

    def patch(self, url, *args, json=None, **kwargs) -> Response:
        return self._client.patch(url, *args, json=json, **kwargs)

    def request(self, method, url, *args, json=None, **kwargs) -> Response:
        return self._client.request(method, url, *args, json=json, **kwargs)

    def delete(self, url, *args, **kwargs) -> Response:
        return self._client.delete(url, *args, **kwargs)

    def stream(self, method, url, *args, **kwargs) -> Iterator[Response]:
        return self._client.stream(method, url, *args, **kwargs)

    @property
    def os_version(self):
        return self._os_version

    @property
    def os_api_version(self):
        return self._os_api_version

    @property
    def user(self) -> User:
        return self._user

    @cached_property
    def _api_insuf_rights(self):
        return api_insuf_rights(self._user)

    @cached_property
    def hostname(self) -> str:
        resp = self.get("/os/hostname")
        if resp.status_code == 200:
            return resp.json()["hostname"]
        else:
            logger.critical(self._api_insuf_rights + 'on GET "/os/hostname"; Using URL host.')
            return str(self.base_url.host)

    @property
    def oas(self) -> Dict[str, Methods]:
        return self._oas.oas

    @property
    def web_to_api(self) -> Dict[str, Endpoint]:
        return self._oas.web_to_api

    @property
    def scope_to_api(self) -> Dict[str, Endpoint]:
        return self._oas.scope_to_api

    @property
    def attribute_filters(self):
        return self._attribute_filters

    @attribute_filters.setter
    def attribute_filters(self, attribute_filters: Union[Dict[str, List[str]], None]):
        if attribute_filters:
            logger.warning(
                "Setting Global Attribute Filter for all tables/diagrams until explicitly unset to None.\n"
                "Adding an Attribute Filter to any function will overwrite the Global Filter.\n"
                f"Filter: {attribute_filters}"
            )
        self._attribute_filters = attribute_filters

    @property
    def snapshots(self) -> OrderedDict[str, Snapshot]:
        return self._snapshots.snapshots

    @property
    def snapshot(self) -> Snapshot:
        return self.snapshots[self.snapshot_id]

    @property
    def loaded_snapshots(self) -> OrderedDict[str, Snapshot]:
        """get only loaded snapshots"""
        return self._snapshots.loaded_snapshots

    @property
    def unloaded_snapshots(self) -> OrderedDict[str, Snapshot]:
        return self._snapshots.unloaded_snapshots

    @property
    def loading_snapshot(self) -> Union[Snapshot, None]:
        """Return Loading Snapshot or None"""
        return self._snapshots.loading_snapshot

    @property
    def running_snapshot(self) -> Union[Snapshot, None]:
        """Return Running Snapshot"""
        return self._snapshots.running_snapshot

    def get_snapshot(self, snapshot_id: str) -> Snapshot:
        return self._snapshots.get_snapshot(snapshot_id)

    def get_snapshot_id(self, snapshot: Union[Snapshot, str]) -> str:
        return self._snapshots.get_snapshot_id(snapshot)

    def get_snapshots(self) -> OrderedDict[str, Snapshot]:
        return self._snapshots.get_snapshots()

    @property
    def prev_snapshot_id(self) -> str:
        """get previous snapshot Id"""
        return self._prev_snapshot_id

    def get_user(self) -> User:
        """Gets current logged in user information.

        Returns:
            User: User model of logged in user
        """
        resp = raise_for_status(self.get("users/me"))
        user = User(**resp.json())
        if not (user.is_admin and user.token):  # TODO: NIM-14008: Implement logic after obtaining scopes for tokens
            resp = raise_for_status(self.get("users/me/scopes/api"))
            user.scopes = resp.json()["data"]
        return user

    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 = raise_for_status(self.post(url, json=payload))
            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

    def trigger_backup(self, sn: str = None, ip: str = None):
        return trigger_backup(self, sn=sn, ip=ip)
