from typing import List, Dict, Any
from typing import Optional
from typing import Union

import httpx
from pydantic import parse_obj_as

from bulk_restore_tool.__about__ import __version__
from bulk_restore_tool._auth import APIClientAuth
from bulk_restore_tool._auth import UserAuth
from bulk_restore_tool.models import Archive
from bulk_restore_tool.models import DetailType
from bulk_restore_tool._settings import BRTSettings

_user_agent = f"bulk-restore-tool {__version__}"


class Client:
    """
    An HTTP client for interacting with Code42 Restore APIs.

    **Parameters**:

    * **url**: `str` The url of your Code42 Console.
    * **api_client_id**: `str` The ID of your [Code42 API Client](https://support.code42.com/Incydr/Admin/Code42_console_reference/API_clients)
    * **api_client_secret**: `str` The Secret for your Code42 API Client.
    """

    def __init__(
        self,
        url: Optional[str] = None,
        username: Optional[str] = None,
        password: Optional[str] = None,
        api_client_id: Optional[str] = None,
        api_client_secret: Optional[str] = None,
        **settings_kwargs,
    ):
        self.settings = BRTSettings(
            url=url,
            username=username,
            password=password,
            api_client_id=api_client_id,
            api_client_secret=api_client_secret,
            **settings_kwargs,
        )
        if self.settings.username:
            auth = UserAuth(  # type: ignore
                url=self.settings.url,
                username=self.settings.username,
                password=self.settings.password.get_secret_value(),  # type: ignore
            )
        else:
            auth = APIClientAuth(  # type: ignore
                url=self.settings.url,
                api_client_id=self.settings.api_client_id,  # type: ignore
                api_client_secret=self.settings.api_client_secret.get_secret_value(),  # type: ignore
            )
        self.config = {
            "auth": auth,
            "headers": {"User-Agent": _user_agent},
            "event_hooks": {
                "response": [lambda response: response.raise_for_status()],
                "request": [],
            },
            "timeout": 60,
        }
        self._httpx = httpx.Client(base_url=self.settings.url, **self.config)  # type: ignore
        # test auth on init to fail fast if invalid
        self._httpx.get("/api/v3/customer/my")
        self.storage_connections = {}  # type: ignore

    def get_storage_connection(self, storage_url: str):
        if storage_url in self.storage_connections:
            return self.storage_connections[storage_url]
        else:
            return self.storage_connections.setdefault(
                storage_url, httpx.Client(**self.config, base_url=storage_url)  # type: ignore
            )

    def create_bulk_restore_session(self, archive: Archive, name: str) -> str:
        storage = self.get_storage_connection(archive.storageNodeWebUrl)
        r = storage.post(
            f"/api/v3/bulk-restore/session/{archive.archiveId}", json={"name": name}
        )
        return r.json()["data"]["bulkRestoreSessionId"]

    def complete_bulk_restore_session(
        self, archive: Archive, session_id: str
    ) -> httpx.Response:
        storage = self.get_storage_connection(archive.storageNodeWebUrl)
        return storage.post(f"/api/v3/bulk-restore/session/{session_id}/complete")

    def get_archive_details(
        self, identifier: str, id_type: Union[str, DetailType]
    ) -> List[Archive]:
        id_type = DetailType(id_type)  # will raise if not valid enum member
        id_map = {
            DetailType.ARCHIVE: "archiveId",
            DetailType.DEVICE: "deviceId",
            DetailType.USER: "username",
            DetailType.LEGAL_HOLD: "legalHoldId",
        }
        r = self._httpx.get(
            "/api/v3/restorable-archive-details", params={id_map[id_type]: identifier}
        )
        archive_list = parse_obj_as(
            List[Archive], r.json()["data"]["restorableArchiveDetails"]
        )
        if not archive_list:
            raise ValueError(f"No archives found for {id_type}={identifier}")
        return archive_list

    def get_archive_metadata(
        self,
        archive: Archive,
        startTime: Optional[int] = None,
        endTime: Optional[int] = None,
        includeDeleted: bool = False,
        includeAllVersions: bool = False,
    ) -> httpx.Response:
        if archive.storageNodeWebUrl is None:
            archive = self.get_archive_details(
                identifier=archive.archiveId, id_type=DetailType.ARCHIVE
            )[0]
        storage = self.get_storage_connection(archive.storageNodeWebUrl)
        params: Dict[str, Any] = {
            "includeDeleted": includeDeleted,
            "includeAllVersions": includeAllVersions,
        }
        if startTime is not None:
            params["startTimeMillis"] = startTime
        if endTime is not None:
            params["endTimeMillis"] = endTime
        return storage.get(
            f"/api/v3/archive-metadata/{archive.archiveId}", params=params
        )

    def get_file(
        self, session_id: str, archive: Archive, fileId: str, versionTimestamp: str
    ) -> httpx.Response:
        if archive.storageNodeWebUrl is None:
            archive = self.get_archive_details(
                identifier=archive.archiveId, id_type=DetailType.ARCHIVE
            )[0]
        storage = self.get_storage_connection(archive.storageNodeWebUrl)
        params = {
            "bulkRestoreSessionId": session_id,
            "archiveGuid": archive.archiveId,
            "fileId": fileId,
            "versionTimestamp": versionTimestamp,
        }
        return storage.get("/api/v3/bulk-restore-file-data", params=params)
