from __future__ import annotations

import functools
import json
import logging
import os
import re
import shutil
import subprocess
import sys
from pathlib import Path
from typing import Tuple

from clarabot_nano_common.config.handler import AbstractConfig
from clarabot_nano_common.utility.path.common import normalize_validate_resolve_absolute_path
from clarabot_nano_common.utility.path.java import java_compatible_absolute_path
from clarabot_nano_common.utility.path.resolve import resolve_path
from clarabot_nano_common.utility.path.validate import ValidationError

from clarabot_nano.common.config.utility import (
    config_get_pid_dir,
    config_get_solr_data_dir,
    config_get_solr_logs_dir,
    config_get_tika_logs_dir,
)
from clarabot_nano.core.config.utility import get_local_license_validity
from clarabot_nano.picoture import PICOTURE_JAR
from clarabot_nano.resource import get_resource_path
from clarabot_nano.utility.java import get_java_version_output, parse_java_major_version

logger = logging.getLogger(__name__)

TIKA_SERVER_JAR_RE = re.compile(r"^tika-server(?:-standard)?-([\d.]+?)\.jar$")


class NanoBundleRootRequired(Exception):
    pass


class Config(AbstractConfig):
    """
    The main config parsing and setup utility. Since an instance of Nano is tightly tied to a specific config
    and an instance shall only run in a single copy, this is a suitable place to implement singleton-application.
    """

    @classmethod
    def _create_local_config_from_template(cls, config_dir, data_dir, logs_dir):
        local_config = super(Config, cls)._create_local_config_from_template(config_dir, data_dir, logs_dir)
        solr_dir = data_dir / "solr"
        tika_dir = data_dir / "tika"
        pid_dir = data_dir / "pid"
        solr_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
        local_config["solr_dir"] = str(solr_dir)
        local_config["tika_dir"] = str(tika_dir)
        local_config["pid_dir"] = str(pid_dir)
        return local_config

    @classmethod
    def _get_resource_path(cls, *parts) -> Path:
        return get_resource_path(*parts)

    def _check_local_values(self):
        self.update_state({"gateway_status": "", "indexer_status": ""})  # make sure they're always str
        self.get_pid_dir()
        self.get_solr_data_dir()
        self.get_solr_logs_dir()
        self.get_tika_logs_dir()
        self._load_vendor_data()

    def _load_vendor_data(self):
        vendor = license = device_id = None
        hook_vendor_data = self._config["local"].get("hook_vendor_data")
        if hook_vendor_data:
            try:
                data = json.loads(
                    subprocess.check_output(
                        hook_vendor_data if isinstance(hook_vendor_data, list) else [hook_vendor_data]
                    )
                )
                if not isinstance(data, dict):
                    raise TypeError("data type not dict")
                vendor = data.get("vendor") or None
                license = data.get("license") or None
                device_id = data.get("device_id") or None
                if (
                    (vendor is not None and not isinstance(vendor, str))
                    or (license is not None and not isinstance(license, str))
                    or (device_id is not None and not isinstance(device_id, str))
                ):
                    raise TypeError("data value invalid")
            except Exception:
                logger.exception("Failed to load vendor data")
        if not isinstance(vendor, str):
            vendor = "Clarabot"
        if license is not None and not isinstance(license, str):
            license = None
        if device_id is not None and not isinstance(device_id, str):
            device_id = None
        validity = get_local_license_validity(vendor, license)
        if isinstance(validity, str):
            expiration = None
        else:
            validity, expiration = validity
        self.update_state(
            {
                "vendor": vendor,
                "license": license,
                "device_id": device_id,
                "license_local_validity": validity,
                "license_expiration": expiration,
                "license_server_validity": "unknown",
            }
        )

    @property
    def localhost_network_security(self) -> str:
        return self._config["local"]["localhost_network_security"]

    @functools.cache
    def get_java_home_executable(self) -> Tuple[str, str]:
        java_executable = "java.exe" if sys.platform.startswith("win") else "java"
        java_dir = self._config["local"]["java_dir"]
        if java_dir:  # prioritize explicit configuration
            explicit = True
            try:
                java_dir = normalize_validate_resolve_absolute_path(java_dir)
            except (OSError, ValidationError):
                raise Exception(f"Configured path for Java is invalid (local.java_dir): {java_dir}")
        else:  # select the first path that seems valid (bundled, JAVA_HOME)
            explicit = False
            try:
                nano_root_dir = self.get_nano_root()
            except NanoBundleRootRequired:
                nano_root_dir = None
            if nano_root_dir and (java_bundle_dir := resolve_path(nano_root_dir.joinpath("deps", "jre"))).is_dir():
                java_dir = java_bundle_dir
            else:
                java_dir = os.environ.get("JAVA_HOME", "")
                if java_dir:
                    try:
                        java_dir = normalize_validate_resolve_absolute_path(java_dir)
                    except (OSError, ValidationError):
                        raise Exception(f"Path in JAVA_HOME environment variable is invalid: {java_dir}")
        if java_dir:  # try to validate the java home if any
            for path in [("bin", "amd64", java_executable), ("bin", java_executable)]:
                java_exe_path = Path(java_dir, *path)
                if java_exe_path.is_file():
                    return java_compatible_absolute_path(java_dir), java_compatible_absolute_path(java_exe_path)
            if explicit:  # report error if the configuration is explicit
                raise Exception(f"Java is not found in configured path (local.java_dir): {java_dir}")
        if not shutil.which(java_executable):  # look for executable in PATH lastly
            raise Exception("Java is not found in PATH")
        java_dir = ""
        return java_dir, java_executable

    @functools.cache
    def get_solr_install_dir(self) -> Path:
        solr_bundle_dir = self._config["local"]["solr_bundle_dir"]
        if solr_bundle_dir:
            try:
                solr_bundle_dir = normalize_validate_resolve_absolute_path(solr_bundle_dir)
            except (OSError, ValidationError):
                raise Exception(f"Configured path for Solr is invalid (local.solr_bundle_dir): {solr_bundle_dir}")
        else:
            try:
                solr_bundle_dir = self.get_nano_root().joinpath("deps", "solr")
                solr_bundle_dir = resolve_path(solr_bundle_dir, strict=True)
            except NanoBundleRootRequired:
                raise Exception("Without Nano bundle root (CBNANO_ROOT), the local.solr_bundle_dir config must be set")
            except (OSError, ValidationError):
                raise Exception(f"Solr is not found in bundled path: {solr_bundle_dir}")
        if not (solr_bundle_dir / "server/start.jar").is_file():
            raise Exception(f"No Solr start jar is found in directory: {solr_bundle_dir / 'server'}")
        return solr_bundle_dir

    @functools.cache
    def get_tika_server_jar(self) -> str:
        tika_bundle_dir = self._config["local"]["tika_bundle_dir"]
        if tika_bundle_dir:
            try:
                tika_bundle_dir = normalize_validate_resolve_absolute_path(tika_bundle_dir)
            except (OSError, ValidationError):
                raise Exception(f"Configured path for Tika is invalid (local.tika_bundle_dir): {tika_bundle_dir}")
        else:
            try:
                tika_bundle_dir = self.get_nano_root().joinpath("deps", "tika")
                tika_bundle_dir = resolve_path(tika_bundle_dir, strict=True)
            except NanoBundleRootRequired:
                raise Exception("Without Nano bundle root (CBNANO_ROOT), the local.tika_bundle_dir config must be set")
            except (OSError, ValidationError):
                raise Exception(f"Tika is not found in bundled path: {tika_bundle_dir}")
        # select most recent version
        jars = []
        for item in tika_bundle_dir.iterdir():
            if item.is_file():
                m = TIKA_SERVER_JAR_RE.match(item.name)
                if m:
                    version = tuple(int(v) for v in m.group(1).split(".") if v)
                    if len(version) == 2:
                        version += (0,)
                    if len(version) == 3:
                        jars.append((item, version))
        if not jars:
            raise Exception(f"No Tika server jar found in directory: {tika_bundle_dir}")
        tika_server_jar = max(jars, key=lambda e: e[1])[0]
        return str(tika_server_jar)

    @functools.cache
    def get_picoture_jar(self) -> str:
        # NOTE: This could be configurable, but it's bundled now.
        try:
            return str(normalize_validate_resolve_absolute_path(PICOTURE_JAR))
        except (OSError, ValidationError):
            raise Exception(f"Picoture is missing from the distribution: {PICOTURE_JAR}")

    def check_dependencies(self):
        _, java_executable = self.get_java_home_executable()
        if parse_java_major_version(get_java_version_output(java_executable)) < 11:
            raise Exception("Java 11 is the minimum required version")
        self.get_solr_install_dir()
        self.get_tika_server_jar()
        self.get_picoture_jar()

    @functools.cache
    def get_pid_dir(self) -> Path:
        return config_get_pid_dir(self._config)

    @functools.cache
    def get_solr_data_dir(self) -> Path:
        return config_get_solr_data_dir(self._config)

    @functools.cache
    def get_solr_logs_dir(self) -> Path:
        return config_get_solr_logs_dir(self._config)

    def get_solr_heap_init(self) -> str:
        return self.local["solr_heap_init"]

    def get_solr_heap_size(self) -> str:
        return self.local["solr_heap_size"]

    @functools.cache
    def get_tika_logs_dir(self) -> Path:
        return config_get_tika_logs_dir(self._config)

    def get_tika_heap_init(self) -> str:
        return self.local["tika_heap_init"]

    def get_tika_heap_size(self) -> str:
        return self.local["tika_heap_size"]

    @functools.cache
    def get_picoture_logs_dir(self) -> Path:
        return Path(self.local["data_dir"], "logs", "picoture")

    def get_picoture_heap_init(self) -> str:
        return self.local["picoture_heap_init"]

    def get_picoture_heap_size(self) -> str:
        return self.local["picoture_heap_size"]

    @functools.cache
    def get_nano_root(self) -> Path:
        """
        The nano's "root" directory is where it is installed and dependencies can be found.
        """
        try:
            return normalize_validate_resolve_absolute_path(os.environ.get("CBNANO_ROOT", ""))
        except Exception as e:
            raise NanoBundleRootRequired("Nano root (CBNANO_ROOT environment variable) is required") from e
