"""Module to decrypt the license using the API."""

import base64
import ast
import glob
import json
import os
from urllib.parse import urlparse
import binascii

import requests
from requests.exceptions import Timeout, RequestException
import yaml

from pywidevine.cdm import Cdm as widevineCdm
from pywidevine.device import Device as widevineDevice
from pywidevine.pssh import PSSH as widevinePSSH
from pyplayready.cdm import Cdm as playreadyCdm
from pyplayready.device import Device as playreadyDevice
from pyplayready.system.pssh import PSSH as playreadyPSSH

from custom_functions.database.unified_db_ops import cache_to_db


def find_license_key(data, keywords=None):
    """Find the license key in the data."""
    if keywords is None:
        keywords = [
            "license",
            "licenseData",
            "widevine2License",
        ]  # Default list of keywords to search for

    # If the data is a dictionary, check each key
    if isinstance(data, dict):
        for key, value in data.items():
            if any(
                keyword in key.lower() for keyword in keywords
            ):  # Check if any keyword is in the key (case-insensitive)
                return value.replace("-", "+").replace(
                    "_", "/"
                )  # Return the value immediately when found
            # Recursively check if the value is a dictionary or list
            if isinstance(value, (dict, list)):
                result = find_license_key(value, keywords)  # Recursively search
                if result:  # If a value is found, return it
                    return result.replace("-", "+").replace("_", "/")

    # If the data is a list, iterate through each item
    elif isinstance(data, list):
        for item in data:
            result = find_license_key(item, keywords)  # Recursively search
            if result:  # If a value is found, return it
                return result.replace("-", "+").replace("_", "/")

    return None  # Return None if no matching key is found


def find_license_challenge(data, keywords=None, new_value=None):
    """Find the license challenge in the data."""
    if keywords is None:
        keywords = [
            "license",
            "licenseData",
            "widevine2License",
            "licenseRequest",
        ]  # Default list of keywords to search for

    # If the data is a dictionary, check each key
    if isinstance(data, dict):
        for key, value in data.items():
            if any(
                keyword in key.lower() for keyword in keywords
            ):  # Check if any keyword is in the key (case-insensitive)
                data[key] = new_value  # Modify the value in-place
            # Recursively check if the value is a dictionary or list
            elif isinstance(value, (dict, list)):
                find_license_challenge(
                    value, keywords, new_value
                )  # Recursively modify in place

    # If the data is a list, iterate through each item
    elif isinstance(data, list):
        for i, item in enumerate(data):
            result = find_license_challenge(
                item, keywords, new_value
            )  # Recursively modify in place

    return data  # Return the modified original data (no new structure is created)


def is_base64(string):
    """Check if the string is base64 encoded."""
    try:
        # Try decoding the string
        decoded_data = base64.b64decode(string)
        # Check if the decoded data, when re-encoded, matches the original string
        return base64.b64encode(decoded_data).decode("utf-8") == string
    except (binascii.Error, TypeError):
        # If decoding or encoding fails, it's not Base64
        return False


def is_url_and_split(input_str):
    """Check if the string is a URL and split it into protocol and FQDN."""
    parsed = urlparse(input_str)

    # Check if it's a valid URL with scheme and netloc
    if parsed.scheme and parsed.netloc:
        protocol = parsed.scheme
        fqdn = parsed.netloc
        return True, protocol, fqdn
    return False, None, None


def load_device(device_type, device, username, config):
    """Load the appropriate device file for PlayReady or Widevine."""
    if device_type == "PR":
        ext, config_key, class_loader = ".prd", "default_pr_cdm", playreadyDevice.load
        base_dir = "PR"
    else:
        ext, config_key, class_loader = ".wvd", "default_wv_cdm", widevineDevice.load
        base_dir = "WV"

    if device == "public":
        base_name = config[config_key]
        if not base_name.endswith(ext):
            base_name += ext
        search_path = f"{os.getcwd()}/configs/CDMs/{base_dir}/{base_name}"
    else:
        base_name = device
        if not base_name.endswith(ext):
            base_name += ext
        search_path = f"{os.getcwd()}/configs/CDMs/{username}/{base_dir}/{base_name}"

    files = glob.glob(search_path)
    if not files:
        return None, f"No {ext} file found for device '{device}'"
    try:
        return class_loader(files[0]), None
    except (IOError, OSError) as e:
        return None, f"Failed to read device file: {e}"
    except (ValueError, TypeError, AttributeError) as e:
        return None, f"Failed to parse device file: {e}"


def prepare_request_data(headers, cookies, json_data, challenge, is_widevine):
    """Prepare headers, cookies, and json_data for the license request."""
    try:
        format_headers = ast.literal_eval(headers) if headers else None
    except (ValueError, SyntaxError) as e:
        raise ValueError(f"Invalid headers format: {e}") from e

    try:
        format_cookies = ast.literal_eval(cookies) if cookies else None
    except (ValueError, SyntaxError) as e:
        raise ValueError(f"Invalid cookies format: {e}") from e

    format_json_data = None
    if json_data and not is_base64(json_data):
        try:
            format_json_data = ast.literal_eval(json_data)
            if is_widevine:
                format_json_data = find_license_challenge(
                    data=format_json_data,
                    new_value=base64.b64encode(challenge).decode(),
                )
        except (ValueError, SyntaxError) as e:
            raise ValueError(f"Invalid json_data format: {e}") from e
        except (TypeError, AttributeError) as e:
            raise ValueError(f"Error processing json_data: {e}") from e

    return format_headers, format_cookies, format_json_data


def send_license_request(license_url, headers, cookies, json_data, challenge, proxies):
    """Send the license request and return the response."""
    try:
        response = requests.post(
            url=license_url,
            headers=headers,
            proxies=proxies,
            cookies=cookies,
            json=json_data if json_data is not None else None,
            data=challenge if json_data is None else None,
            timeout=10,
        )
        return response, None
    except ConnectionError as error:
        return None, f"Connection error: {error}"
    except Timeout as error:
        return None, f"Request timeout: {error}"
    except RequestException as error:
        return None, f"Request error: {error}"


def extract_and_cache_keys(
    cdm,
    session_id,
    cache_to_db,
    pssh,
    license_url,
    headers,
    cookies,
    challenge,
    json_data,
    is_widevine,
):
    """Extract keys from the session and cache them."""
    returned_keys = ""
    try:
        keys = list(cdm.get_keys(session_id))
        for index, key in enumerate(keys):
            # Widevine: key.type, PlayReady: key.key_type
            key_type = getattr(key, "type", getattr(key, "key_type", None))
            kid = getattr(key, "kid", getattr(key, "key_id", None))
            if key_type != "SIGNING" and kid is not None:
                cache_to_db(
                    pssh=pssh,
                    license_url=license_url,
                    headers=headers,
                    cookies=cookies,
                    data=challenge if json_data is None else json_data,
                    kid=kid.hex,
                    key=key.key.hex(),
                )
                if index != len(keys) - 1:
                    returned_keys += f"{kid.hex}:{key.key.hex()}\n"
                else:
                    returned_keys += f"{kid.hex}:{key.key.hex()}"
        return returned_keys, None
    except AttributeError as error:
        return None, f"Error accessing CDM keys: {error}"
    except (TypeError, ValueError) as error:
        return None, f"Error processing keys: {error}"


def api_decrypt(
    pssh: str = "",
    license_url: str = "",
    proxy: str = "",
    headers: str = "",
    cookies: str = "",
    json_data: str = "",
    device: str = "public",
    username: str = "",
):
    """Decrypt the license using the API."""
    print(f"Using device {device} for user {username}")
    with open(f"{os.getcwd()}/configs/config.yaml", "r", encoding="utf-8") as file:
        config = yaml.safe_load(file)

    if pssh == "":
        return {"status": "error", "message": "No PSSH provided"}

    # Detect PlayReady or Widevine
    try:
        is_pr = "</WRMHEADER>".encode("utf-16-le") in base64.b64decode(pssh)
    except (binascii.Error, TypeError) as error:
        return {
            "status": "error",
            "message": f"An error occurred processing PSSH\n\n{error}",
        }

    device_type = "PR" if is_pr else "WV"
    cdm_class = playreadyCdm if is_pr else widevineCdm
    pssh_class = playreadyPSSH if is_pr else widevinePSSH

    # Load device
    device_obj, device_err = load_device(device_type, device, username, config)
    if device_obj is None:
        return {"status": "error", "message": device_err}

    # Create CDM
    try:
        cdm = cdm_class.from_device(device_obj)
    except (IOError, ValueError, AttributeError) as error:
        return {
            "status": "error",
            "message": f"An error occurred loading {device_type} CDM\n\n{error}",
        }

    # Open session
    try:
        session_id = cdm.open()
    except (IOError, ValueError, AttributeError) as error:
        return {
            "status": "error",
            "message": f"An error occurred opening a CDM session\n\n{error}",
        }

    # Parse PSSH and get challenge
    try:
        pssh_obj = pssh_class(pssh)
        if is_pr:
            challenge = cdm.get_license_challenge(session_id, pssh_obj.wrm_headers[0])
        else:
            challenge = cdm.get_license_challenge(session_id, pssh_obj)
    except (ValueError, AttributeError, IndexError) as error:
        return {
            "status": "error",
            "message": f"An error occurred getting license challenge\n\n{error}",
        }

    # Prepare request data
    try:
        format_headers, format_cookies, format_json_data = prepare_request_data(
            headers, cookies, json_data, challenge, is_widevine=(not is_pr)
        )
    except (ValueError, SyntaxError) as error:
        return {
            "status": "error",
            "message": f"An error occurred preparing request data\n\n{error}",
        }

    # Prepare proxies
    proxies = ""
    if proxy != "":
        is_url, protocol, fqdn = is_url_and_split(proxy)
        if is_url:
            proxies = {"http": proxy, "https": proxy}
        else:
            return {
                "status": "error",
                "message": "Your proxy is invalid, please put it in the format of http(s)://fqdn.tld:port",
            }

    # Send license request
    licence, req_err = send_license_request(
        license_url,
        format_headers,
        format_cookies,
        format_json_data,
        challenge,
        proxies,
    )
    if licence is None:
        return {"status": "error", "message": req_err}

    # Parse license
    try:
        if is_pr:
            cdm.parse_license(session_id, licence.text)
        else:
            try:
                cdm.parse_license(session_id, licence.content)  # type: ignore[arg-type]
            except (ValueError, TypeError):
                # Try to extract license from JSON
                try:
                    license_json = licence.json()
                    license_value = find_license_key(license_json)
                    if license_value is not None:
                        cdm.parse_license(session_id, license_value)
                    else:
                        return {
                            "status": "error",
                            "message": f"Could not extract license from JSON: {license_json}",
                        }
                except (ValueError, json.JSONDecodeError, AttributeError) as error:
                    return {
                        "status": "error",
                        "message": f"An error occurred parsing license content\n\n{error}\n\n{licence.content}",
                    }
    except (ValueError, TypeError, AttributeError) as error:
        return {
            "status": "error",
            "message": f"An error occurred parsing license content\n\n{error}\n\n{licence.content}",
        }

    # Extract and cache keys
    returned_keys, key_err = extract_and_cache_keys(
        cdm,
        session_id,
        cache_to_db,
        pssh,
        license_url,
        headers,
        cookies,
        challenge,
        json_data,
        is_widevine=(not is_pr),
    )
    if returned_keys == "":
        return {"status": "error", "message": key_err}

    # Close session
    try:
        cdm.close(session_id)
    except (IOError, ValueError, AttributeError) as error:
        return {
            "status": "error",
            "message": f"An error occurred closing session\n\n{error}",
        }

    return {"status": "success", "message": returned_keys}
