import logging
from datetime import timedelta

from core.choices import JobStatusChoices
from core.exceptions import SyncError
from dcim.models import Site
from dcim.models import VirtualChassis
from dcim.signals import assign_virtualchassis_master

try:
    # Got added in NetBox 4.4.9
    from dcim.signals import sync_cached_scope_fields
except ImportError:
    sync_cached_scope_fields = None
from django.db.models import signals
from netbox.context_managers import event_tracking
from rq.timeouts import JobTimeoutException
from utilities.datetime import local_now
from utilities.request import NetBoxFakeRequest

from .choices import IPFabricSourceStatusChoices
from .choices import IPFabricSyncStatusChoices
from .models import IPFabricIngestion
from .models import IPFabricSnapshot
from .models import IPFabricSource
from .models import IPFabricSync
from .utilities.logging import SyncLogging

logger = logging.getLogger(__name__)


def safe_save_job_data(job, obj_with_logger):
    """
    Safely save logger data to job.data field.
    Catches and logs any exceptions to avoid masking the original error.

    Args:
        job: The Job instance to save data to
        obj_with_logger: An object (IPFabricSource, IPFabricSync, or IPFabricIngestion.sync)
                        that has a 'logger' attribute with 'log_data'
    """
    try:
        if hasattr(obj_with_logger, "logger") and hasattr(
            obj_with_logger.logger, "log_data"
        ):
            job.data = obj_with_logger.logger.log_data
            job.save(update_fields=["data"])
    except Exception as save_error:
        logger.warning(f"Failed to save job data for job {job.pk}: {save_error}")


def sync_ipfabricsource(job, *args, **kwargs):
    ipfsource = IPFabricSource.objects.get(pk=job.object_id)

    try:
        job.start()
        ipfsource.sync(job=job)
        safe_save_job_data(job, ipfsource)
        job.terminate()
    except Exception as e:
        safe_save_job_data(job, ipfsource)
        job.terminate(status=JobStatusChoices.STATUS_ERRORED)
        IPFabricSource.objects.filter(pk=ipfsource.pk).update(
            status=IPFabricSourceStatusChoices.FAILED
        )
        if type(e) in (SyncError, JobTimeoutException):
            logging.error(e)
        else:
            raise e


def sync_ipfabric(job, *args, **kwargs):
    sync = IPFabricSync.objects.get(pk=job.object_id)

    # ------------------------------------------------------------------
    # When enqueue_sync_job passes a pending_snapshot_id the ingestion
    # job is declared dependent on the source-sync job via RQ depends_on,
    # so this job only starts after the source sync has completed and the
    # snapshot is guaranteed to be present in the database.
    # The DoesNotExist branch is kept as a hard-fail safety net (e.g. if
    # IP Fabric removed the snapshot before the ingestion job ran).
    # ------------------------------------------------------------------
    pending_snapshot_id = kwargs.get("pending_snapshot_id")
    if pending_snapshot_id:
        try:
            snapshot = IPFabricSnapshot.objects.get(
                source=sync.snapshot_data.source,
                snapshot_id=pending_snapshot_id,
            )
            # Redirect this run to the requested snapshot.
            IPFabricSync.objects.filter(pk=sync.pk).update(snapshot_data=snapshot)
            sync.snapshot_data = snapshot
            logger.info(
                "sync_ipfabric '%s': redirected to snapshot '%s'.",
                sync.name,
                pending_snapshot_id,
            )
        except IPFabricSnapshot.DoesNotExist:
            logger.error(
                "sync_ipfabric '%s' failed: snapshot '%s' was not found. "
                "The snapshot may have been removed from IP Fabric before "
                "the ingestion job started.",
                sync.name,
                pending_snapshot_id,
            )
            safe_save_job_data(job, sync)
            job.terminate(status=JobStatusChoices.STATUS_ERRORED)
            return

    try:
        job.start()
        sync.sync(job=job, main_version=kwargs.get("ipf_version"))
        safe_save_job_data(job, sync)
        job.terminate()
    except Exception as e:
        safe_save_job_data(job, sync)
        job.terminate(status=JobStatusChoices.STATUS_ERRORED)
        IPFabricSync.objects.filter(pk=sync.pk).update(
            status=IPFabricSyncStatusChoices.FAILED
        )
        if type(e) in (SyncError, JobTimeoutException):
            logging.error(e)
        else:
            raise e
    finally:
        if sync.interval and not kwargs.get("adhoc"):
            new_scheduled_time = local_now() + timedelta(minutes=sync.interval)
            # We want to create new Job only if scheduled time was before this Job started
            # The current sync might have been changed while this job was running
            sync.refresh_from_db()
            if not sync.scheduled or (
                sync.scheduled
                and sync.scheduled > job.started
                and sync.jobs.filter(
                    status__in=[
                        JobStatusChoices.STATUS_SCHEDULED,
                        JobStatusChoices.STATUS_PENDING,
                        JobStatusChoices.STATUS_RUNNING,
                    ]
                )
                .exclude(pk=job.pk)
                .exists()
            ):
                logger.info(
                    f"Not scheduling a new job for IPFabricSync {sync.pk} as the scheduled time was changed while the job was running."
                )
                return
            # Update the sync object with the new scheduled time
            # This also triggers the creation of a new Job
            # Running in fake request context to ensure user is set for changelog
            request = NetBoxFakeRequest(
                {
                    "META": {},
                    "POST": sync.parameters,
                    "GET": {},
                    "FILES": {},
                    "user": sync.user,
                    "path": "",
                    "id": job.job_id,
                }
            )

            with event_tracking(request):
                sync.scheduled = new_scheduled_time
                sync.status = IPFabricSyncStatusChoices.QUEUED
                sync.full_clean()
                sync.save()
            logger.info(
                f"Scheduled next sync for IPFabricSync {sync.pk} at {new_scheduled_time}."
            )


def merge_ipfabric_ingestion(job, remove_branch=False, *args, **kwargs):
    ingestion = IPFabricIngestion.objects.get(pk=job.object_id)
    try:
        request = NetBoxFakeRequest(
            {
                "META": {},
                "POST": ingestion.sync.parameters,
                "GET": {},
                "FILES": {},
                "user": ingestion.sync.user,
                "path": "",
                "id": job.job_id,
            }
        )

        job.start()
        # Persist the merge job PK so IPFabricIngestion.get_merge_logs() can
        # find this job's cache entry when building merge statistics.
        ingestion.merge_job = job
        ingestion.save(update_fields=["merge_job"])
        # SyncLogging keyed to the merge job PK — this is the cache entry that
        # get_merge_logs() reads (ipfabric_sync_{merge_job_pk}).
        ingestion.sync.logger = SyncLogging(job=job.pk)
        with event_tracking(request):
            try:
                # This signal is disabled on sync, we need to disable it here too
                signals.post_save.disconnect(
                    assign_virtualchassis_master, sender=VirtualChassis
                )
                if sync_cached_scope_fields is not None:
                    signals.post_save.disconnect(sync_cached_scope_fields, sender=Site)
                ingestion.sync_merge()
            finally:
                # Re-enable the disabled signals
                signals.post_save.connect(
                    assign_virtualchassis_master, sender=VirtualChassis
                )
                if sync_cached_scope_fields is not None:
                    signals.post_save.connect(sync_cached_scope_fields, sender=Site)
        if remove_branch and ingestion.sync.status != IPFabricSyncStatusChoices.FAILED:
            branching_branch = ingestion.branch
            ingestion.branch = None
            ingestion.save()
            branching_branch.delete()

        safe_save_job_data(job, ingestion.sync)
        job.terminate()
    except Exception as e:
        logger.exception(
            f"Error during merge for IPFabricIngestion {ingestion.pk}: {e}"
        )
        safe_save_job_data(job, ingestion.sync)
        job.terminate(status=JobStatusChoices.STATUS_ERRORED)
        IPFabricSync.objects.filter(pk=ingestion.sync.pk).update(
            status=IPFabricSyncStatusChoices.FAILED
        )
        if type(e) in (SyncError, JobTimeoutException):
            logging.error(e)
        else:
            raise e
