"""Job and dataset utilities"""

import enum
import re
import time
import unicodedata
import uuid

import marshmallow_dataclass
from marshmallow import pre_load
from qgis.PyQt import QtCore
from te_schemas.algorithms import AlgorithmRunMode, ExecutionScript
from te_schemas.jobs import Job as JobBase
from te_schemas.jobs import RemoteScript

from .. import api, conf
from ..logger import log


class SortField(enum.Enum):
    NAME = "name"
    DATE = "date"
    ALGORITHM = "algorithm"
    STATUS = "status"


class TypeFilter(enum.Enum):
    ALL = "all"
    RASTER = "raster"
    VECTOR = "vector"


def _slugify(value, allow_unicode=False):
    """
    Taken from https://github.com/django/django/blob/master/django/utils/text.py
    Convert to ASCII if 'allow_unicode' is False. Convert spaces or repeated
    dashes to single dashes. Remove characters that aren't alphanumerics,
    underscores, or hyphens. Convert to lowercase. Also strip leading and
    trailing whitespace, dashes, and underscores.
    """
    value = str(value)

    if allow_unicode:
        value = unicodedata.normalize("NFKC", value)
    else:
        value = (
            unicodedata.normalize("NFKD", value)
            .encode("ascii", "ignore")
            .decode("ascii")
        )
    value = re.sub(r"[^\w\s-]", "", value.lower())

    return re.sub(r"[-\s]+", "-", value).strip("-_")


@marshmallow_dataclass.dataclass
class Job(JobBase):
    @pre_load
    def set_script_name_version(self, data, **kwargs):
        script_id = data.pop("script_id", None)
        params_script = data["params"].pop("script", None)

        if not data.get("script"):
            if params_script:
                script = ExecutionScript.Schema().load(params_script)
            elif script_id:
                try:
                    script = _get_job_script(script_id)
                except IndexError:
                    # Fallback: try to get script by name from data if available
                    script_name = data.get("script_name")
                    if script_name:
                        script = get_job_local_script(script_name)
                        if not script:
                            script = ExecutionScript(
                                script_id, run_mode=AlgorithmRunMode.NOT_APPLICABLE
                            )
                    else:
                        script = ExecutionScript(
                            script_id, run_mode=AlgorithmRunMode.NOT_APPLICABLE
                        )
            else:
                script = ExecutionScript(
                    "Unknown script", run_mode=AlgorithmRunMode.NOT_APPLICABLE
                )

            data["script"] = ExecutionScript.Schema().dump(script)

        script_name_regex = re.compile("([0-9a-zA-Z -]*)(?: *)([0-9]+(_[0-9]+)+)")
        matches = script_name_regex.search(data["script"].get("name"))

        if matches:
            data["script"]["name"] = matches.group(1).rstrip()
            data["script"]["version"] = matches.group(2).replace("_", ".")

        return data

    def get_basename(self, with_uuid=False):
        separator = "_"
        name_fragments = []
        task_name = _slugify(self.task_name)

        if task_name:
            name_fragments.append(task_name)

        if self.script:
            name_fragments.append(self.script.name)

        if self.local_context.area_of_interest_name:
            name_fragments.append(self.local_context.area_of_interest_name)

        if len(name_fragments) == 0 or with_uuid:
            name_fragments.append(str(self.id))

        return separator.join(name_fragments)

    def get_display_name(self):
        job_name_parts = []
        if self.task_name:
            job_name_parts.append(self.task_name)
        elif self.local_context.area_of_interest_name:
            job_name_parts.append(self.local_context.area_of_interest_name)
        elif self.script:
            # Use name for display (shorter), fallback to name_readable if name not available
            script_display_name = (
                self.script.name if self.script.name else self.script.name_readable
            )
            if script_display_name:
                job_name_parts.append(script_display_name)
        return " - ".join(job_name_parts)

    # ------------------------------------------------------------------
    # Cache-aware overrides: when jobs are loaded from SQLite without
    # results data, the _cached_* attributes are set by the cache layer.
    # Check them first so the UI can determine job type without needing
    # the heavy results pickle.
    # ------------------------------------------------------------------

    def is_vector(self) -> bool:
        cached = getattr(self, "_cached_is_vector", None)
        if cached is not None:
            return cached
        return super().is_vector()

    def is_raster(self) -> bool:
        cached = getattr(self, "_cached_is_raster", None)
        if cached is not None:
            return cached
        return super().is_raster()

    def is_timeseries(self) -> bool:
        cached = getattr(self, "_cached_is_timeseries", None)
        if cached is not None:
            return cached
        return super().is_timeseries()

    def is_file(self) -> bool:
        cached = getattr(self, "_cached_is_file", None)
        if cached is not None:
            return cached
        return super().is_file()


# Cache for remote scripts: (result, timestamp)
_remote_scripts_cache = None
_remote_scripts_cache_time = 0.0
_REMOTE_SCRIPTS_CACHE_TTL = 300  # 5 minutes
_remote_scripts_mutex = QtCore.QMutex()


def get_remote_scripts():
    """Query the remote server for existing scripts

    The information returned from the remote shall be enough to at least
    display Jobs which came from a version which is not known locally.

    Results are cached for 5 minutes. Failed lookups (None) are not cached
    so that transient network errors don't permanently prevent script loading.

    A mutex serialises concurrent callers so the cache is not populated by
    multiple threads simultaneously.
    """
    global _remote_scripts_cache, _remote_scripts_cache_time

    _remote_scripts_mutex.lock()
    try:
        now = time.monotonic()
        if (
            _remote_scripts_cache is not None
            and (now - _remote_scripts_cache_time) < _REMOTE_SCRIPTS_CACHE_TTL
        ):
            return _remote_scripts_cache

        raw_remote = api.get_default_api_client().get_script()

        if raw_remote is None:
            return None

        result = [RemoteScript.Schema().load(r) for r in raw_remote]
        _remote_scripts_cache = result
        _remote_scripts_cache_time = now
        return result
    finally:
        _remote_scripts_mutex.unlock()


def get_job_local_script(script_name: str) -> ExecutionScript:
    result = conf.KNOWN_SCRIPTS.get(script_name, None)
    return result


def _get_script_by_id_from_remote(script_id: str) -> ExecutionScript:
    remote_scripts = get_remote_scripts()
    if remote_scripts is None:
        return
    try:
        script = [s for s in remote_scripts if str(s.id) == str(script_id)][0]
    except IndexError:
        log(
            f"script {script_id!r} not found on remote server "
            "by id - checking for matches by slug"
        )
        try:
            script = [s for s in remote_scripts if str(s.slug) == str(script_id)][0]
        except IndexError:
            raise IndexError
    script = ExecutionScript.Schema().load(RemoteScript.Schema().dump(script))

    return script


def _get_script_by_id_from_local(script_id: str) -> ExecutionScript:
    try:
        script = [
            s for s in conf.KNOWN_SCRIPTS.values() if str(s.id) == str(script_id)
        ][0]
        return script
    except IndexError:
        raise IndexError

    return script


def _get_job_script(script_id: uuid.UUID) -> ExecutionScript:
    try:
        script = _get_script_by_id_from_local(str(script_id))
    except IndexError:
        try:
            script = _get_script_by_id_from_remote(str(script_id))
        except IndexError:
            raise IndexError

    return script
