import os
import subprocess
import sys
from pathlib import Path

import psutil

from clarabot_nano.core.config.handler import TIKA_SERVER_JAR_RE
from clarabot_nano.core.spawn.poll import (
    AbstractSpawnHealthPoller,
    AbstractPolledSpawnWithThread,
    AbstractSpawnHealthHTTPPoller,
)
from clarabot_nano.core.spawn.track import SubprocessTracker
from clarabot_nano.resource import get_resource_path
from clarabot_nano.utility.network import get_free_port_for_dynamic_service
from clarabot_nano.utility.path.java import java_compatible_absolute_path


class TikaSpawnHealthPoller(AbstractSpawnHealthHTTPPoller):
    def __init__(self, spawn):
        AbstractSpawnHealthHTTPPoller.__init__(self, spawn)

    def _check_process_health(self, spawn) -> bool:
        try:
            with self._session.get(f"http://{spawn.host}:{spawn.port}/version", timeout=8.0) as r:
                if r.status_code == 200:
                    return r.text.startswith("Apache Tika ")
        except Exception:
            pass
        return False


class TikaSpawn(AbstractPolledSpawnWithThread):
    """
    Tika cannot start on a random port by itself, so we're hedging our bets and try to start it on random ports
    until we find one that works. In case the tika port would get used up by something else, this is still better
    than having it on a static port and having it never work anymore.
    """

    PID_PREFIX = "tika-server"

    def __init__(
        self,
        java_home: str,
        java_executable: str,
        tika_server_jar: str,
        tika_logs_dir: Path,
        tika_pid_dir: Path,
        tika_heap_size: str,
    ):
        # home & executable shall be supplied in java-compatible paths
        self._java_home = java_home
        self._java_executable = java_executable
        self._tika_server_jar = java_compatible_absolute_path(tika_server_jar)
        self._tika_logs_dir = java_compatible_absolute_path(tika_logs_dir)
        self._tika_pid_dir = tika_pid_dir
        self._tika_heap_size = tika_heap_size
        self.host = "127.0.0.1"
        self.port = get_free_port_for_dynamic_service()
        self._tika_v1 = TIKA_SERVER_JAR_RE.match(os.path.basename(self._tika_server_jar)).group(1).startswith("1.")
        AbstractPolledSpawnWithThread.__init__(self)

    def _create_health_poller(self) -> AbstractSpawnHealthPoller:
        return TikaSpawnHealthPoller(self)

    def kill(self):
        """
        The "stop" command already includes a kill after a timeout so nothing to be done here.
        """
        pass

    def get_pid_prefix(self) -> str:
        return self.PID_PREFIX

    def _stop_spawn(self, *args):
        for _ in SubprocessTracker.cleanup(
            self._tika_pid_dir, kill_timeout=10.0, pid_file_prefix=self.get_pid_prefix()
        ):
            pass  # cannot ask graceful shutdown though api, just return and do it by terminate-signal

    def _start_spawn(self, *args) -> psutil.Popen:
        # prepare spawn
        tika_spawn_config_dir = Path(java_compatible_absolute_path(get_resource_path("spawn", "tika")))
        logs_dir = Path(self._tika_logs_dir)
        logs_dir.mkdir(parents=True, exist_ok=True)
        env = dict(os.environ)
        if self._java_home:
            env["JAVA_HOME"] = self._java_home
        java_mem = [f"-Xms{self._tika_heap_size}", f"-Xmx{self._tika_heap_size}"]
        gc_tune = [
            "-XX:+UseG1GC",
            "-XX:+PerfDisableSharedMem",
            "-XX:+ParallelRefProcEnabled",
            "-XX:MaxGCPauseMillis=250",
            "-XX:+AlwaysPreTouch",
        ]
        if not sys.platform.startswith("win"):
            gc_tune.append("-XX:+UseLargePages")
        java_security_manager = [
            # "-Djava.security.debug=access",  # for development
            "-Djava.security.manager",
            f"-Djava.security.policy={tika_spawn_config_dir / 'security.policy'}",
            # f"-Djava.security.properties={tika_spawn_config_dir / 'security.properties'}",
        ]
        if self._tika_v1:
            log_args = [
                # "-Dlog4j.debug=true",
                # NOTE: The "file:" URL prefix purposefully omits the "//". There is a bug on Windows, where the path
                # is not well parsed and it becomes "\\C\...".
                f"-Dlog4j.configuration=file:{tika_spawn_config_dir / 'log4j.properties'}",
            ]
        else:
            log_args = [
                # "-Dlog4j2.debug=true",  # for development
                f"-Dlog4j2.configurationFile={tika_spawn_config_dir / 'log4j2.xml'}",
                "-Dlog4j2.formatMsgNoLookups=true",  # mitigate CVE-2021-44228 for Log4j2<=2.14.1 and Java<=8u121
            ]
        # Compile launch args for the tika server.
        launch_args = [
            self._java_executable,
            *java_mem,
            *gc_tune,
            *java_security_manager,
            *log_args,
            f"-Dnano.config.tika={tika_spawn_config_dir}",
            f"-Dtika.logs.dir={self._tika_logs_dir}",
            # f"-Xlog:gc*:file={self._tika_logs_dir}/tika_gc.log:time,uptime:filecount=3,filesize=20M",
            "-jar",
            self._tika_server_jar,
            "-h",
            self.host,
            "-p",
            str(self.port),
        ]
        # NOTE: Worker processes are not reliably restarted unfortunately. 50% of the time an OOM error
        # in a worker will break the server (503 Service Unavailable). The core will respawn it, but this
        # makes the forking mostly useless since we have it run by an overseer process anyways.
        # The custom Java setup and security.manager would not be applied to them either with 1.x. Have
        # not checked how 2.x spawns workers, but keep it similar for now.
        if not self._tika_v1:
            launch_args.append("-noFork")
        with open(logs_dir / "tika-server-stdout.log", "w") as log_file:
            p = self._start_reap_popen(launch_args, env=env, cwd=logs_dir, stdout=log_file, stderr=subprocess.STDOUT)
        SubprocessTracker.record(
            self._tika_pid_dir, p, metadata={"port": self.port}, pid_file_prefix=self.get_pid_prefix()
        )
        return p
