import os
import secrets
import time
import urllib
import webbrowser
from importlib.resources import files
from pathlib import Path
from time import sleep

import platformdirs
import requests
import yaml
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

from whiffle_client.auth import (
    AUTH_STATE,
    ServerThread,
    auth0_url_encode,
    generate_challenge,
)
from whiffle_client.decorators import request_ok, with_token


class BaseClient:
    """
    Base class client to connect to Whiffle APIs
    """

    # API variables
    ENDPOINTS_URL: str = ""

    CONFIG_FILE_NAME = "whiffle_config.yaml"
    CONFIG_PACKAGE_FILE_LOCATION = (files("whiffle_client") / "resources" / CONFIG_FILE_NAME).as_posix()
    CONFIG_USER_FILE_LOCATION = f"{platformdirs.user_config_dir('whiffle')}/{CONFIG_FILE_NAME}"

    config: dict = {}

    # Type method
    def __init__(self, access_token=None, token_config=None, url=None, session=None):
        """
        Initialize the client.

        Authentication order:
        1. `access_token` passed when creating class.
        2. `token_config` passed when creating class.
        3. token in CONFIG_FILE_PATH_LOCATION (JSON format)

        Parameters
        ----------
        access_token : str, optional
            Token for client session auth
        token_config : dict(str), optional
            Token configuration for client session auth
        url : str, optional
            Url pointing to API
        """

        self.config = self.get_config()
        if access_token:
            self.update_token_config(token={"access_token": access_token}, persist=False)
        if token_config:
            self.update_token_config(token=token_config, persist=False)

        access_token_validator = self.config["user"].get("token_validator", None)
        access_token = self.config["user"]["access_token"]

        url = url or self.config["whiffle"]["url"]
        self.server_url = url

        if session is None:
            # More docs: https://urllib3.readthedocs.io/en/latest/reference/urllib3.util.html#urllib3.util.Retry
            status_forcelist = (500, 502, 503, 504)
            retry = Retry(
                total=5,  # Total number of retries to allow
                backoff_factor=0.1,  # Incremental time between retry requests
                status_forcelist=status_forcelist,  # A set of integer HTTP status codes that will force a retry on
            )
            adapter = HTTPAdapter(max_retries=retry)
            session = requests.Session()
            session.mount("http://", adapter)
            session.mount("https://", adapter)
        self.session = session

        self.session.headers = {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {access_token}",
            "Authorization-type": ("Integration" if access_token_validator == "zitadel" else None),
        }

    def __repr__(self) -> str:
        return f"Whiffle wind client connected to url: {self.server_url}"

    def update_token_config(self, token, persist=True):
        self.config = self.get_config()
        self.config["user"]["access_token"] = token["access_token"]
        self.config["user"]["refresh_token"] = token.get("refresh_token", "")
        self.config["user"]["token_validator"] = token.get("token_validator", "manual")
        self.config["user"]["expires_in"] = token.get("expires_in", 3600)
        self.config["user"]["obtained_at"] = token.get("obtained_at", int(time.time()))
        if persist:
            self.set_config(config=self.config)

    def get_token(self):
        IDP_BASE_URL = self.config["whiffle"]["idp_base_url"]
        CLIENT_ID = self.config["whiffle"]["client_id"]
        REDIRECT_URI = self.config["whiffle"]["redirect_uri"]

        verifier = auth0_url_encode(secrets.token_bytes(32))
        challenge = generate_challenge(verifier)
        base_url = IDP_BASE_URL + "/authorize?"
        url_parameters = {
            "client_id": CLIENT_ID,
            "redirect_uri": REDIRECT_URI,
            "scope": "profile openid email urn:zitadel:iam:user:resourceowner offline_access",
            "response_type": "code",
            "response_mode": "query",
            "code_challenge_method": "S256",
            "code_challenge": challenge.replace("=", ""),
            "state": auth0_url_encode(secrets.token_bytes(32)),
        }
        # Open the browser window to the login url
        webbrowser.open_new(base_url + urllib.parse.urlencode(url_parameters))
        server = ServerThread()
        # Start the server
        server.start()
        print(f"Waiting for log in callback")
        print(f"If no browser opens, please visit: {base_url}{urllib.parse.urlencode(url_parameters)}")

        # Poll until the callback has been invoked or timeout
        waited = 2
        sleep(2)  # Usually in two seconds token has been refreshed
        while not AUTH_STATE["received_callback"] and waited < 60:
            sleep(1)
            waited += 1
            print(
                f"Waiting for callback for {waited} seconds, status {AUTH_STATE['received_callback']}",
                end="\r",
                flush=True,
            )
        if not AUTH_STATE["received_callback"]:
            raise RuntimeError("Timeout waiting for log in callback")
        server.shutdown()

        # received_state and error_message are global variable from auth_app callback
        if url_parameters["state"] != AUTH_STATE["received_state"]:
            raise RuntimeError(
                "Error: session replay or similar attack in progress. Please log out of all connections."
            )
        if AUTH_STATE["error_message"]:
            raise RuntimeError("An error occurred: {}".format(AUTH_STATE["error_message"]))
        # Exchange the code for a token
        headers = {"Content-Type": "application/x-www-form-urlencoded"}
        body = {
            "grant_type": "authorization_code",
            "code": AUTH_STATE["code"],  # global variable from auth_app callback
            "redirect_uri": REDIRECT_URI,
            "client_id": CLIENT_ID,
            "code_verifier": verifier,
        }
        token_request = requests.post(IDP_BASE_URL + "token", headers=headers, data=body)
        if token_request.status_code != 200:
            raise RuntimeError(f"Error: failed to exchange code for token {token_request.status_code}")

        token_request = token_request.json()
        token_request["token_validator"] = "zitadel"
        self.update_token_config(token_request, persist=True)

    def refresh_token(self):
        IDP_BASE_URL = self.config["whiffle"]["idp_base_url"]
        CLIENT_ID = self.config["whiffle"]["client_id"]

        if "refresh_token" not in self.config["user"] or not self.config["user"]["refresh_token"]:
            raise RuntimeError("No refresh token available, must log in again.")

        headers = {"Content-Type": "application/x-www-form-urlencoded"}
        data = {
            "grant_type": "refresh_token",
            "refresh_token": self.config["user"]["refresh_token"],
            "client_id": CLIENT_ID,
        }
        token_refresh_response = requests.post(IDP_BASE_URL + "token", headers=headers, data=data)

        if token_refresh_response.status_code != 200:
            raise RuntimeError(
                f"Failed to refresh token: {token_refresh_response.status_code} {token_refresh_response.text}"
            )

        token_refresh_response = token_refresh_response.json()
        token_refresh_response["token_validator"] = "zitadel"
        self.update_token_config(token_refresh_response, persist=True)

    def get_valid_access_token(self):
        """Return a valid access token, refreshing if needed."""
        # NOTE: use already provided config or load it again
        if not self.config["user"]["access_token"]:
            self.get_token()
            self.config = self.get_config()

        if time.time() > self.config["user"]["obtained_at"] + self.config["user"]["expires_in"] - 30:
            self.refresh_token()
            self.config = self.get_config()

        return self.config["user"]["access_token"]

    def merge_configurations(self, base_config: dict, update_config: dict) -> dict:
        for key, value in update_config.items():
            if key not in base_config:
                base_config[key] = value
            elif isinstance(value, dict) and isinstance(base_config.get(key), dict):
                self.merge_configurations(base_config[key], value)

        return base_config

    def get_config(self) -> dict:
        """Gathers client configuration from local and updates with resources one.

        Returns
        -------
        dict
            Dictionary containing the configuration.

        Raises
        ------
        FileNotFoundError
            Raises error if no configuration found
        """
        with open(self.CONFIG_PACKAGE_FILE_LOCATION) as file_object:
            config = yaml.safe_load(file_object)

        # Merge default config with user config if present
        user_config = {}
        if Path(self.CONFIG_USER_FILE_LOCATION).exists():
            with open(self.CONFIG_USER_FILE_LOCATION) as file_object:
                user_config = yaml.safe_load(file_object)

        return self.merge_configurations(user_config, config)

    def set_config(self, config):
        config_file_path = self.CONFIG_USER_FILE_LOCATION
        os.makedirs(os.path.dirname(config_file_path), exist_ok=True)
        with open(config_file_path, "w") as file_object:
            yaml.safe_dump(config, file_object)

    @request_ok
    @with_token
    def get_request(self, url, **kwargs):
        return self.session.get(url, **kwargs)

    @request_ok
    @with_token
    def post_request(self, url, data=None, files=None):
        if files:
            return self.session.post(url, data=data, files=files)

        return self.session.post(url, json=data)

    @request_ok
    @with_token
    def put_request(self, url, data=None, files=None):
        if files:
            return self.session.put(url, data=data, files=files)

        return self.session.put(url, json=data)

    @request_ok
    @with_token
    def delete_request(self, url):
        return self.session.delete(url)
