"""Event handler module"""

import datetime
import logging
import time
from pathlib import Path
from typing import Dict, Union

import apscheduler
import yaml
from apscheduler.job import Job
from apscheduler.schedulers.background import BackgroundScheduler
from watchdog.events import (
    DirCreatedEvent,
    DirDeletedEvent,
    FileCreatedEvent,
    FileDeletedEvent,
    FileSystemEventHandler,
)

from aind_watchdog_service.models.manifest_config import (
    IngestedManifest,
    check_for_missing_data,
)
from aind_watchdog_service.models.watch_config import WatchConfig
from aind_watchdog_service.run_job import RunJob


class EventHandler(FileSystemEventHandler):
    """Event handler for watchdog observer"""

    def __init__(self, scheduler: BackgroundScheduler, config: WatchConfig):
        """Initialize event handler"""
        super().__init__()
        self.scheduler = scheduler
        self.config = config
        self.jobs: Dict[str, Job] = {}
        self._startup_manifest_check()

    def _startup_manifest_check(self) -> None:
        """ " Check for manifests to process in the manifest directory on startup"""
        manifest_dir = Path(self.config.flag_dir).glob("*manifest*.*")
        for manifest in manifest_dir:
            transfer_config = self._load_manifest(str(manifest))
            if transfer_config:
                self.schedule_job(str(manifest), transfer_config)

    def _load_manifest(self, src_path: str) -> IngestedManifest | None:
        """Instructions to transfer to VAST"""
        logging.info("Loading manifest %s", src_path)
        with open(src_path, "r", encoding="utf-8") as f:
            try:
                data = yaml.safe_load(f)
                checksum_mode = (
                    data.get("checksum_mode_override", None)
                    or self.config.checksum_parameters.mode
                )

                config = IngestedManifest(**data, checksum_mode=checksum_mode)
                if len(config.modalities) == 0:
                    raise ValueError("No data in manifest, empty 'modality' argument")
            except Exception:
                logging.exception(
                    f"Error loading manifest, manifest_file, {src_path}",
                    extra={"emit_exc": True},
                )
                return

        # Check for missing files
        missing_files, missing_schema = check_for_missing_data(config)

        if missing_files or missing_schema:
            logging.warning(
                {
                    "Warning": "Missing files when loading manifest",
                    "Missing data": missing_files,
                    "Missing schema": missing_schema,
                }
                | config.log_tags
            )

        return config

    def _get_trigger_time(self, transfer_time: datetime.time) -> datetime.datetime:
        """Get trigger time from the job

        Parameters
        ----------
        transfer_time : datetime.time
            time to trigger the job
        Returns
        -------
        datetime.datetime
            datetime object
        """
        _now = datetime.datetime.now().astimezone()

        if transfer_time.tzinfo is None or transfer_time.utcoffset() is None:
            # Treat naive transfer times as local wall-clock times.
            transfer_time = transfer_time.replace(tzinfo=_now.tzinfo)

        # Normalize combined datetime to local timezone-aware datetime.
        trigger_time = datetime.datetime.combine(_now.date(), transfer_time).astimezone(
            _now.tzinfo
        )

        trigger_time = (
            trigger_time
            if trigger_time > _now
            else trigger_time + datetime.timedelta(days=1)
        )
        logging.info("Trigger time %s", trigger_time)
        return trigger_time

    def schedule_job(self, src_path: str, job_config: IngestedManifest) -> None:
        """Schedule job to run

        Parameters
        ----------
        src_path : str
            manifest file path to trigger
        config : dict
            configuration for the job
        """
        if not job_config.schedule_time:
            # logging.info("Scheduling job to run now %s", src_path)
            run = RunJob(src_path, job_config, self.config)
            job_id = self.scheduler.add_job(
                run.run_job,
                misfire_grace_time=self.config.misfire_grace_time_s,
            )

        else:
            trigger = self._get_trigger_time(job_config.safe_scheduled_time)
            # logging.info("Scheduling job to run at %s %s", trigger, src_path)
            run = RunJob(src_path, job_config, self.config)
            job_id = self.scheduler.add_job(
                run.run_job,
                "date",
                run_date=trigger,
                misfire_grace_time=self.config.misfire_grace_time_s,
            )
        logging.info(
            {
                "Action": "Job Scheduled",
                "Scheduled Time": job_config.schedule_time or "now",
            }
            | job_config.log_tags,
            extra={"weblog": True},
        )

        self.jobs[src_path] = job_id

    def remove_job(self, src_path: str) -> None:
        """Remove job from scheduler if it already exists"""
        if src_path in self.jobs:
            logging.info("Deleting job %s", src_path)
            try:
                self.scheduler.remove_job(self.jobs[src_path].id)
                logging.info(
                    {
                        "Action": "Manifest deleted",
                        "File": src_path,
                    },
                    extra={"weblog": True},
                )
            except apscheduler.jobstores.base.JobLookupError:
                logging.info(
                    "No apscheduler job for %s, this probably means the job ran successfully and this deletion is part of the move to manifest_completed",
                    src_path,
                )
            del self.jobs[src_path]
        logging.info("Jobs in queue %s", self.scheduler.get_jobs())

    def on_deleted(self, event: Union[FileDeletedEvent, DirDeletedEvent]) -> None:
        """Event handler for file deleted event

        Parameters
        ----------
        event : FileDeletedEvent | DirDeletedEvent
            file deleted event

        Returns
        -------
        None
        """
        self.remove_job(event.src_path)

    def on_modified(self, event) -> None:
        """Event handler for file modified event"""
        return self.on_created(event)

    def on_created(self, event: Union[FileCreatedEvent, DirCreatedEvent]) -> None:
        """Event handler for file created event
        Parameters
        ----------
        event : FileCreatedEvent | DirCreatedEvent
            file created event
        Returns
        -------
        None
        """
        # Check if manifest file is being modified / created
        _path = Path(str(event.src_path))
        if isinstance(event, DirCreatedEvent) | _path.is_dir():
            return
        if "manifest" not in _path.name or _path.suffix not in [".yml", ".yaml"]:
            return
        # If scheduled manifest is being modified, remove original job
        if event.src_path in self.jobs:
            self.remove_job(event.src_path)
        logging.info("Found event file %s", event.src_path)  # log schedule time
        time.sleep(10)  # Wait for file to be written
        transfer_config = self._load_manifest(event.src_path)
        if transfer_config:
            self.schedule_job(event.src_path, transfer_config)
        print(self.scheduler.get_jobs())
        pass
