import re
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from requests import Session
from typing import Any, Dict, List
import os
import requests
import json


retry_strategy = Retry(
    total=2,
    backoff_factor=1,
    status_forcelist=[429, 500, 502, 503, 504],
    method_whitelist=["HEAD", "GET", "PUT", "POST", "DELETE", "OPTIONS", "TRACE"],
)
retry_adapter = HTTPAdapter(max_retries=retry_strategy)
requests_retry = Session()
requests_retry.mount("https://", retry_adapter)
requests_retry.mount("http://", retry_adapter)
tempdir_ttl_days = 1


def assert_valid_name(name: str):
    is_valid = re.match(r"^[A-Za-z0-9_]+$", name)
    if not is_valid:
        raise Exception(
            f"'{name}' must only contain alphanumeric and underscore characters."
        )


def raise_resp_exception_error(resp):
    if not resp.ok:
        message = None
        try:
            r_body = resp.json()
            message = r_body.get("message") or r_body.get("msg")
        except:
            # If we failed for whatever reason (parsing body, etc.)
            # Just return the code
            if resp.status_code == 500:
                raise Exception(
                    f"HTTP Error received: {resp.reason}: {str(resp.status_code)}"
                )
            else:
                raise Exception(
                    f"HTTP Error received: {resp.reason}: {str(resp.status_code)} | {resp.json()['detail']}"
                )
        if message:
            raise Exception(f"Error: {message}")
        else:
            if resp.status_code == 500:
                raise Exception(
                    f"HTTP Error received: {resp.reason}: {str(resp.status_code)}"
                )
            else:
                raise Exception(
                    f"HTTP Error received: {resp.reason}: {str(resp.status_code)} | {resp.json()['detail']}"
                )


def chunks(lst, n):
    """Yield successive n-sized chunks from lst."""
    for i in range(0, len(lst), n):
        yield lst[i : i + n]


def _upload_local_files(
    file_names: List[str],
    get_upload_path: str,
    headers: Dict[str, Any],
    upload_prefix: str,
    upload_suffix: str,
    delete_after_upload: bool = True,
):
    """This uploads a set of files in batches with a reader.

    Args:
        file_names (str): The local file_names (files to be uploaded)
        get_upload_path (str): The URL that generates upload URLs
        headers (Dict[str, Any]): Headers for the get_upload_path request
        upload_prefix (str): Prefix for the filepath (once uploaded)
        upload_suffix (str): Suffix for the filepath (once uploaded)
        delete_after_upload (bool): Whether to delete the file after upload

    Return:
        A list of download URLs for the uploaded files
    """
    xml_api_headers = {
        "content-type": "application/octet-stream",
    }

    download_urls = []
    if len(file_names) == 0:
        return download_urls

    url_chunks = chunks(file_names, 100)

    param_batch_list = []
    for i, chunk in enumerate(url_chunks, start=1):
        for count, file_name in enumerate(chunk, start=1):

            upload_filename, ext = os.path.splitext(os.path.basename(file_name))

            param_batch_list.append(
                {"file_name": upload_filename, "file_extension": ext}
            )

            if count == len(chunk):

                data = {"files": param_batch_list}

                upload_urls_resp = requests_retry.post(
                    get_upload_path,
                    headers=headers,
                    data=json.dumps(data).encode("utf-8"),
                )
                raise_resp_exception_error(upload_urls_resp)
                batch_of_urls = upload_urls_resp.json()
                param_batch_list = []
        # batch_of_urls comes back in the same expected order as the chunk, so we zip and enumerate simultaneously.
        for count, (file_name, urlobj) in enumerate(zip(chunk, batch_of_urls), start=1):
            with open(file_name, "rb") as f:
                files = {"file": (file_name, f)}
                http_response = requests.post(
                    urlobj["url"], data=urlobj["fields"], files=files
                )

    return True

def _upload_local_zip(
    zip_filepath: str,
    get_upload_path: str,
    complete_upload_path: str,
    headers: Dict[str, Any],
    upload_prefix: str,
    upload_suffix: str,
    delete_after_upload: bool = True,
):
    """This uploads a set of files in batches with a reader.

    Args:
        file_names (str): The local file_names (files to be uploaded)
        get_upload_path (str): The URL that generates upload URLs
        headers (Dict[str, Any]): Headers for the get_upload_path request
        upload_prefix (str): Prefix for the filepath (once uploaded)
        upload_suffix (str): Suffix for the filepath (once uploaded)
        delete_after_upload (bool): Whether to delete the file after upload

    Return:
        A list of download URLs for the uploaded files
    """
    xml_api_headers = {
        "content-type": "application/octet-stream",
    }

    download_urls = []

    upload_filename, ext = os.path.splitext(os.path.basename(zip_filepath))

    max_size = 1 * 1024 * 1024 * 1024 # 5GB is max, we use 1GB
    #TODO: calculate parts obj_size/max_size
    parts_no = 1
    payload = {'parts': parts_no}
    upload_url_parts_resp = requests_retry.get(
        get_upload_path,
        headers=headers,
        params=payload
    )
    raise_resp_exception_error(upload_url_parts_resp)
    upload_id = upload_url_parts_resp.json()[0]
    batch_of_urls = upload_url_parts_resp.json()[1]
    param_batch_list = []
    parts_tracker = []

    for part_no, url in enumerate(batch_of_urls, start=1):
        with open(zip_filepath, "rb") as f:
            file_data = f.read(max_size)
            response = requests.put(url, data=file_data)

            etag = response.headers['ETag']

            parts_tracker.append({'ETag': etag, 'PartNumber': part_no})

    data = {'parts': parts_tracker}
    comp_resp = requests_retry.post(
        complete_upload_path + '/' + upload_id,
        headers=headers,
        data=json.dumps(data).encode("utf-8")
    )

    return comp_resp