"""
Required Notice: Copyright (C) Zoomer Analytics GmbH.

xlwings PRO is dual-licensed under one of the following licenses:

* PolyForm Noncommercial License 1.0.0 (for noncommercial use):
  https://polyformproject.org/licenses/noncommercial/1.0.0
* xlwings PRO License (for commercial use):
  https://github.com/xlwings/xlwings/blob/main/LICENSE_PRO.txt

Commercial licenses can be purchased at https://www.xlwings.org
"""

import os
import base64
import hmac
import hashlib
import json
import binascii
import datetime as dt
import warnings
import xlwings  # To prevent circular imports
import time
import glob
import shutil
import tempfile
import atexit
from functools import lru_cache

from ..utils import read_config_sheet

try:
    from cryptography.fernet import Fernet, InvalidToken
except ImportError as e:
    Fernet = None
    InvalidToken = None


class LicenseHandler:
    @staticmethod
    def get_cipher():
        try:
            return Fernet('kS2PJbJuXbNXC-sY9XFGg01OmEKuCnKAH0zyMNWWm8c=')
        except (TypeError, ValueError):
            raise xlwings.LicenseError("Couldn't validate license key.") from None

    @staticmethod
    def get_license():
        # Sheet config (only used by RunPython, UDFs use env var)
        try:
            sheet_license_key = read_config_sheet(xlwings.Book.caller()).get(
                "LICENSE_KEY"
            )
            if sheet_license_key:
                return sheet_license_key
        except:
            pass
        # User config file
        config_file = xlwings.USER_CONFIG_FILE
        if os.path.exists(config_file):
            with open(config_file, "r") as f:
                config = f.readlines()
            key = None
            for line in config:
                if line.split(",")[0] == '"LICENSE_KEY"':
                    key = line.split(",")[1].strip()[1:-1]
            if key:
                return key
        # Env Var - also used if LICENSE_KEY is in config sheet and called via UDF
        if os.getenv("XLWINGS_LICENSE_KEY"):
            return os.environ["XLWINGS_LICENSE_KEY"]
        raise xlwings.LicenseError("Couldn't find a license key.")

    @staticmethod
    @lru_cache()
    def validate_license(product, license_type=None):
        key = LicenseHandler.get_license()
        if key == "noncommercial":
            return {"license_type": "noncommercial"}
        if key.startswith("gA") and not Fernet:
            # Legacy up to 0.27.12
            raise ImportError(
                "You are using a legacy license key that requires the 'cryptography' "
                "package. Alternatively, contact us for a new license key."
            ) from None
        elif key.startswith("gA"):
            cipher_suite = LicenseHandler.get_cipher()
            try:
                license_info = json.loads(cipher_suite.decrypt(key.encode()).decode())
            except (binascii.Error, InvalidToken):
                raise xlwings.LicenseError("Invalid license key.") from None
        else:
            signature = hmac.new(
                'kS2PJbJuXbNXC-sY9XFGg01OmEKuCnKAH0zyMNWWm8c='.encode(),
                key[:-5].encode(),
                hashlib.sha256,
            ).hexdigest()
            if signature[:5] != key[-5:]:
                raise xlwings.LicenseError("Invalid license key.") from None
            else:
                try:
                    license_info = json.loads(
                        base64.urlsafe_b64decode(key[:-5]).decode()
                    )
                except:
                    raise xlwings.LicenseError("Invalid license key.") from None
        try:
            if (
                license_type == "developer"
                and license_info["license_type"] != "developer"
            ):
                raise xlwings.LicenseError(
                    "You need a developer license for this action."
                )
        except KeyError:
            raise xlwings.LicenseError(
                "You need a developer license for this action."
            ) from None
        if (
            "valid_until" not in license_info.keys()
            or "products" not in license_info.keys()
        ):
            raise xlwings.LicenseError("Invalid license key format.") from None
        license_valid_until = dt.datetime.strptime(
            license_info["valid_until"], "%Y-%m-%d"
        ).date()
        if dt.date.today() > license_valid_until:
            raise xlwings.LicenseError(
                "Your license expired on {}.".format(
                    license_valid_until.strftime("%Y-%m-%d")
                )
            ) from None
        if product not in license_info["products"]:
            raise xlwings.LicenseError(
                f"License key isn't valid for the '{product}' functionality."
            ) from None
        if (
            "version" in license_info.keys()
            and license_info["version"] != xlwings.__version__
        ):
            raise xlwings.LicenseError(
                "Your license key is only valid for xlwings v{0}".format(
                    license_info["version"]
                )
            ) from None
        if (license_valid_until - dt.date.today()) < dt.timedelta(days=30):
            warnings.warn(
                f"Your license key expires in "
                f"{(license_valid_until - dt.date.today()).days} days."
            )
        return license_info

    @staticmethod
    def create_deploy_key():
        license_info = LicenseHandler.validate_license("pro", license_type="developer")
        if license_info["license_type"] == "noncommercial":
            return "noncommercial"
        license_dict = json.dumps(
            {
                "version": xlwings.__version__,
                "products": license_info["products"],
                "valid_until": "2999-12-31",
                "license_type": "deploy_key",
            }
        ).encode()
        if LicenseHandler.get_license().startswith("gA"):
            # Legacy
            cipher_suite = LicenseHandler.get_cipher()
            return cipher_suite.encrypt(license_dict).decode()
        else:
            body = base64.urlsafe_b64encode(license_dict)
            signature = hmac.new(
                'kS2PJbJuXbNXC-sY9XFGg01OmEKuCnKAH0zyMNWWm8c='.encode(), body, hashlib.sha256
            ).hexdigest()
            return f"{body.decode()}{signature[:5]}"


def get_embedded_code_temp_dir():
    tmp_base_path = os.path.join(tempfile.gettempdir(), "xlwings")
    os.makedirs(tmp_base_path, exist_ok=True)
    try:
        # HACK: Clean up directories that are older than 30 days
        # This should be done in the C++ part when the Python process is killed
        for subdir in glob.glob(tmp_base_path + "/*/"):
            if os.path.getmtime(subdir) < time.time() - 30 * 86400:
                shutil.rmtree(subdir, ignore_errors=True)
    except Exception:
        pass  # we don't care if it fails
    tempdir = tempfile.mkdtemp(dir=tmp_base_path)
    # This only works for RunPython calls running outside the COM server
    atexit.register(shutil.rmtree, tempdir)
    return tempdir
