import logging
from http import HTTPStatus
from typing import Any, Optional, Tuple
from urllib.parse import urlparse

import requests

from arthurai.client.validation import validate_response_status
from arthurai.common.exceptions import (
    ArthurUnexpectedError,
    ForbiddenError,
    UserValueError,
)
from arthurai.core.auth_info import AuthInfo

JSON_CONTENT_TYPE = "application/json"

logger = logging.getLogger(__name__)


def construct_url(*parts: str, validate=True, default_https=True) -> str:
    """Construct a url from various parts

    Useful for joining pieces which may or may not have leading and/or trailing
    slashes. e.g. construct_url("http://arthur.ai/", "/api/v3", "/users") will yield the same valid url as
    construct_url("http://arthur.ai", "api/v3/", "users/").

    :param validate: if True, validate that the URL is valid
    :param default_https: if True, allow urls without a scheme and use https by default
    :param parts: strings from which to construct the url
    :return: a fully joined url
    """
    # join parts
    url = "/".join(s.strip("/") for s in parts)

    # add scheme
    parsed_url = urlparse(url)
    if parsed_url.scheme is None or parsed_url.scheme == "":
        if default_https:
            logger.warning("No url scheme provided, defaulting to https")
            url = "https://" + url
            parsed_url = urlparse(url)
        elif validate:
            raise UserValueError(f"No scheme provided in URL {url}")

    # validate
    if validate and (
        parsed_url.scheme is None
        or parsed_url.scheme == ""
        or parsed_url.netloc is None
        or parsed_url.netloc == ""
    ):
        joiner = "', '"
        raise UserValueError(
            f"Invalid url, cannot construct URL from parts '{joiner.join(parts)}'"
        )

    return url


def get_current_org(
    api_http_host: str, api_prefix: str, access_key: str, verify_ssl: bool = True
) -> Optional[str]:
    """Get the current organization for the provided access key

    :param api_http_host: base url of the host to connect to, including protocol (e.g. "https://app.arthur.ai")
    :param api_prefix: prefix of the API to connect to (e.g. "/api/v3")
    :param access_key: API Key to pass to the API
    :param verify_ssl: Boolean for whether requests should verify that the SSL certificate is valid and not self-signed
    :return: the organization ID associated with the provided access key, None if no such organization exists
    """
    try:
        internal_user_org = get_arthur_internal_user_org(
            api_http_host, api_prefix, access_key, verify_ssl
        )
    except ForbiddenError:
        auth_info = get_auth_info(api_http_host, api_prefix, access_key, verify_ssl)
        if auth_info is None:
            raise ArthurUnexpectedError("Invalid / non-existent auth_info")
        elif len(auth_info.organization_ids) == 1:
            # If the user is authenticated into a single org, automatically select it as the user's current org
            return auth_info.organization_ids[0]
        elif len(auth_info.organization_ids) > 1:
            # Raise exception and give the user options of available current organizations
            authenticated_org_id_str = "".join(
                f"{org_id}\n" for org_id in auth_info.organization_ids
            )[:-1]
            raise UserValueError(
                f"Your access_key provides access to multiple organizations - please specify one of the following:\n{authenticated_org_id_str}"
            )
        else:  # len(auth_info.organization_ids) == 0
            return None

    # Valid internal_user_org was found
    return internal_user_org


def get_auth_info(
    api_http_host: str, api_prefix: str, access_key: str, verify_ssl: bool = True
) -> Optional[AuthInfo]:
    """Get the AuthInfo struct associated with the provided access key

    :param api_http_host: base url of the host to connect to, including protocol (e.g. "https://app.arthur.ai")
    :param api_prefix: prefix of the API to connect to (e.g. "/api/v3")
    :param access_key: API token to pass to the API
    :param verify_ssl: Boolean for whether requests should verify that the SSL certificate is valid and not self-signed
    :return: the AuthInfo associated with the provided access key
    :permissions: N/A
    """
    url = construct_url(api_http_host, api_prefix, "/users/me/auth_info")
    headers = {"Authorization": access_key, "Accept": JSON_CONTENT_TYPE}

    try:
        resp = requests.get(url, headers=headers, verify=verify_ssl)
    except requests.exceptions.SSLError as e:
        raise UserValueError(
            f"""SSL Error connecting to {api_http_host}, please connect to a secure server or use 
                             verify_ssl=False to override security checks"""
        ) from e
    except requests.RequestException as e:
        raise UserValueError(
            f"Failed to connect to {api_http_host}, please ensure the URL is correct"
        ) from e
    if resp.status_code == HTTPStatus.UNAUTHORIZED:
        raise UserValueError("Unauthorized, please ensure your access key is correct")
    if resp.status_code == HTTPStatus.NOT_FOUND:
        raise ArthurUnexpectedError("Auth Info endpoint not implemented by api-host")

    validate_response_status(resp, HTTPStatus.OK)
    return AuthInfo.from_json(resp.text)


def get_arthur_internal_user_org(
    api_http_host: str, api_prefix: str, access_key: str, verify_ssl: bool = True
) -> Optional[str]:
    """Get the current organization for the provided Arthur access key belonging to an Arthur internal user

    :param api_http_host: base url of the host to connect to, including protocol (e.g. "https://app.arthur.ai")
    :param api_prefix: prefix of the API to connect to (e.g. "/api/v3")
    :param access_key: API Key to pass to the API
    :param verify_ssl: Boolean for whether requests should verify that the SSL certificate is valid and not self-signed
    :return: the organization ID associated with the provided access key, None if no such organization exists
    :permissions: N/A
    """
    url = construct_url(api_http_host, api_prefix, "/users/me")
    headers = {"Authorization": access_key, "Accept": JSON_CONTENT_TYPE}

    try:
        resp = requests.get(url, headers=headers, verify=verify_ssl)
    except requests.exceptions.SSLError as e:
        raise UserValueError(
            f"""SSL Error connecting to {api_http_host}, please connect to a secure server or use 
                             verify_ssl=False to override security checks"""
        ) from e
    except requests.RequestException as e:
        raise UserValueError(
            f"Failed to connect to {api_http_host}, please ensure the URL is correct"
        ) from e
    if resp.status_code == HTTPStatus.UNAUTHORIZED:
        raise UserValueError("Unauthorized, please ensure your access key is correct")
    if resp.status_code == HTTPStatus.FORBIDDEN:
        # The only case in which /users/me would return a Forbidden error response is if the calling user
        # is not an Arthur internal user (e.g. using a 3rd party identity provider)
        raise ForbiddenError("Caller is not an Arthur internal user")
    validate_response_status(resp, HTTPStatus.OK)
    response_body = resp.json()
    return response_body.get("organization_id", None)


def user_login(
    api_http_host: str,
    api_prefix: str,
    login: str,
    password: str,
    verify_ssl: bool = True,
) -> Tuple[Optional[str], Any]:
    """Get the current organization for the provided access key

    :param api_http_host: base url of the host to connect to, including protocol (e.g. "https://app.arthur.ai")
    :param api_prefix: prefix of the API to connect to (e.g. "/api/v3")
    :param login: the username or password to use to log in
    :param password: password for the user
    :param verify_ssl: Boolean for whether requests should verify that the SSL certificate is valid and not self-signed
    :return: a tuple of (access_key, {user object})
    """
    url = construct_url(api_http_host, api_prefix, "/login")
    headers = {"Accept": JSON_CONTENT_TYPE}
    body = {"login": login, "password": password}
    try:
        resp = requests.post(url, headers=headers, json=body, verify=verify_ssl)
    except requests.exceptions.SSLError as e:
        raise UserValueError(
            f"""SSL Error connecting to {api_http_host}, please connect to a secure server or use 
                             verify_ssl=False to override security checks"""
        ) from e
    except requests.RequestException as e:
        raise UserValueError(
            f"Failed to connect to {api_http_host}, please ensure the URL is correct"
        ) from e
    if resp.status_code == HTTPStatus.UNAUTHORIZED:
        raise UserValueError(
            f"Unauthorized, please ensure your username and password are correct"
        )

    validate_response_status(resp, HTTPStatus.OK)

    return resp.cookies.get("Authorization"), resp.json()
