"""Scheduled background jobs.

Registers periodic tasks on an APScheduler ``BackgroundScheduler``:

* **MITRE sync** — every 24 hours (see :func:`sync_mitre`)
* **Intel scan** — every 7 days (see :func:`scan_intel`)

Each job manages its own database session (created on entry, closed in
``finally``) so it is fully independent from FastAPI's request-scoped
sessions.
"""

# Import logging
import logging
from datetime import datetime, timedelta, timezone

# Import BackgroundScheduler from apscheduler.schedulers.background
from apscheduler.schedulers.background import BackgroundScheduler

# Import SessionLocal from app.database
from app.database import SessionLocal

# Import sync_all_jira_links from app.jobs.jira_sync_job
from app.jobs.jira_sync_job import sync_all_jira_links

# Import run_retention_job from app.jobs.retention_job
from app.jobs.retention_job import run_retention_job

# Import check_and_run_recurring_campaigns from app.services.campaign_scheduler_service
from app.services.campaign_scheduler_service import check_and_run_recurring_campaigns

# Import scan_intel from app.services.intel_service
from app.services.intel_service import scan_intel

# Import sync_mitre from app.services.mitre_sync_service
from app.services.mitre_sync_service import sync_mitre

# Import cleanup_old_notifications from app.services.notification_service
from app.services.notification_service import cleanup_old_notifications

# Import enrich_all_techniques from app.services.osint_enrichment_service
from app.services.osint_enrichment_service import enrich_all_techniques

# Import cleanup_old_snapshots, create_snapshot from app.services.snapshot_service
from app.services.snapshot_service import cleanup_old_snapshots, create_snapshot

# Import detect_stale_coverage from app.services.stale_detection_service
from app.services.stale_detection_service import detect_stale_coverage

# Assign logger = logging.getLogger(__name__)
logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Module-level scheduler instance
# ---------------------------------------------------------------------------

scheduler = BackgroundScheduler()


# ---------------------------------------------------------------------------
# Job functions
# ---------------------------------------------------------------------------


def _run_mitre_sync() -> None:
    """Execute a MITRE sync inside its own DB session."""
    from app.services.webhook_service import dispatch_webhook
    logger.info("Scheduled MITRE sync job starting...")
    # Assign db = SessionLocal()
    db = SessionLocal()
    # Attempt the following; catch errors below
    try:
        # Assign summary = sync_mitre(db)
        summary = sync_mitre(db)
        # Log info: "Scheduled MITRE sync job finished — %s", summary
        logger.info("Scheduled MITRE sync job finished — %s", summary)
        dispatch_webhook("mitre.synced", {"created": summary.get("created", 0), "updated": summary.get("updated", 0)})
    except Exception:
        # Log exception: "Scheduled MITRE sync job failed"
        logger.exception("Scheduled MITRE sync job failed")
    # Always execute this cleanup block
    finally:
        # Close the database session
        db.close()


# Define function _run_notification_cleanup
def _run_notification_cleanup() -> None:
    """Clean up old read notifications."""
    # Log info: "Scheduled notification cleanup job starting..."
    logger.info("Scheduled notification cleanup job starting...")
    # Assign db = SessionLocal()
    db = SessionLocal()
    # Attempt the following; catch errors below
    try:
        # Assign deleted = cleanup_old_notifications(db, days=90)
        deleted = cleanup_old_notifications(db, days=90)
        # Log info: "Notification cleanup finished — deleted %d old no
        logger.info("Notification cleanup finished — deleted %d old notifications", deleted)
    # Handle Exception
    except Exception:
        # Log exception: "Notification cleanup job failed"
        logger.exception("Notification cleanup job failed")
    # Always execute this cleanup block
    finally:
        # Close the database session
        db.close()


# Define function _run_weekly_snapshot
def _run_weekly_snapshot() -> None:
    """Create a weekly coverage snapshot and clean up old ones."""
    # Log info: "Scheduled weekly snapshot job starting..."
    logger.info("Scheduled weekly snapshot job starting...")
    # Assign db = SessionLocal()
    db = SessionLocal()
    # Attempt the following; catch errors below
    try:
        # Assign snapshot = create_snapshot(db, name="Auto-weekly")
        snapshot = create_snapshot(db, name="Auto-weekly")
        # Log info:
        logger.info(
            # Literal argument value
            "Weekly snapshot created — score %.1f, %d techniques",
            snapshot.organization_score,
            snapshot.total_techniques,
        )
        # Assign deleted = cleanup_old_snapshots(db, keep_last=52)
        deleted = cleanup_old_snapshots(db, keep_last=52)
        # Check: deleted
        if deleted:
            # Log info: "Cleaned up %d old snapshots", deleted
            logger.info("Cleaned up %d old snapshots", deleted)
    # Handle Exception
    except Exception:
        # Log exception: "Weekly snapshot job failed"
        logger.exception("Weekly snapshot job failed")
    # Always execute this cleanup block
    finally:
        # Close the database session
        db.close()


# Define function _run_recurring_campaigns
def _run_recurring_campaigns() -> None:
    """Check and run any due recurring campaigns."""
    # Log info: "Scheduled recurring campaigns check starting..."
    logger.info("Scheduled recurring campaigns check starting...")
    # Assign db = SessionLocal()
    db = SessionLocal()
    # Attempt the following; catch errors below
    try:
        # Assign spawned = check_and_run_recurring_campaigns(db)
        spawned = check_and_run_recurring_campaigns(db)
        # Log info: "Recurring campaigns check finished — spawned %d c
        logger.info("Recurring campaigns check finished — spawned %d campaigns", spawned)
    # Handle Exception
    except Exception:
        # Log exception: "Recurring campaigns check failed"
        logger.exception("Recurring campaigns check failed")
    # Always execute this cleanup block
    finally:
        # Close the database session
        db.close()


def _run_intel_scan() -> None:
    """Execute an intel scan inside its own DB session."""
    # Log info: "Scheduled intel scan job starting..."
    logger.info("Scheduled intel scan job starting...")
    # Assign db = SessionLocal()
    db = SessionLocal()
    # Attempt the following; catch errors below
    try:
        # Assign summary = scan_intel(db)
        summary = scan_intel(db)
        # Log info: "Scheduled intel scan job finished — %s", summary
        logger.info("Scheduled intel scan job finished — %s", summary)
    # Handle Exception
    except Exception:
        # Log exception: "Scheduled intel scan job failed"
        logger.exception("Scheduled intel scan job failed")
    # Always execute this cleanup block
    finally:
        # Close the database session
        db.close()


def _run_evaluation_round_check() -> None:
    """Weekly job: check if a new ATT&CK Evaluation round is available.

    If a new round is found it is imported automatically and an admin
    notification is created so the team knows new baseline data is available.
    """
    logger.info("ATT&CK Evaluations new-round check starting...")
    db = SessionLocal()
    try:
        from app.services.attck_evaluations_service import check_for_new_round, import_evaluation_round
        from app.models.user import User as UserModel

        result = check_for_new_round(db)
        if result.get("error"):
            logger.warning("ATT&CK Evaluations check failed: %s", result["error"])
            return

        if not result.get("new_round_available"):
            logger.info(
                "ATT&CK Evaluations check — latest round '%s' already imported",
                result.get("latest_round", {}).get("display_name", "?"),
            )
            return

        latest = result["latest_round"]
        logger.info(
            "New ATT&CK Evaluation round detected: %s (round %d) — starting auto-import",
            latest["display_name"], latest["eval_round"],
        )

        # Use the first admin user as the importer (system action)
        admin = db.query(UserModel).filter(UserModel.role == "admin").first()
        if not admin:
            logger.warning("ATT&CK Evaluations auto-import: no admin user found — skipping")
            return

        summary = import_evaluation_round(
            db,
            latest["name"],
            latest["display_name"],
            latest["eval_round"],
            admin,
        )
        logger.info(
            "ATT&CK Evaluations auto-import complete — round %d (%s): %d tests created",
            latest["eval_round"], latest["display_name"], summary["created"],
        )

        # Notify all admins
        try:
            from app.services.notification_service import create_notification
            admins = db.query(UserModel).filter(UserModel.role == "admin").all()
            for adm in admins:
                create_notification(
                    db,
                    user_id=adm.id,
                    title="New ATT&CK Evaluation round imported",
                    message=(
                        f"Round {latest['eval_round']} — {latest['display_name']} — "
                        f"has been automatically imported. "
                        f"{summary['created']} tests created in In Review state. "
                        f"Blue Leads must validate each result before it counts as coverage."
                    ),
                    notification_type="eval_import",
                    entity_type="evaluation",
                    entity_id=None,
                )
            db.commit()
        except Exception:
            logger.warning("Failed to send eval import notifications", exc_info=True)

    except Exception:
        logger.exception("ATT&CK Evaluations round check job failed")
    finally:
        db.close()


def _run_osint_enrichment() -> None:
    """Execute weekly OSINT enrichment inside its own DB session."""
    # Log info: "Scheduled OSINT enrichment job starting..."
    logger.info("Scheduled OSINT enrichment job starting...")
    # Assign db = SessionLocal()
    db = SessionLocal()
    # Attempt the following; catch errors below
    try:
        # Assign total = enrich_all_techniques(db)
        total = enrich_all_techniques(db)
        # Log info: "OSINT enrichment finished — %d new items", total
        logger.info("OSINT enrichment finished — %d new items", total)
    # Handle Exception
    except Exception:
        # Log exception: "OSINT enrichment job failed"
        logger.exception("OSINT enrichment job failed")
    # Always execute this cleanup block
    finally:
        # Close the database session
        db.close()


_FREQUENCY_INTERVALS: dict[str, timedelta] = {
    "daily": timedelta(days=1),
    "weekly": timedelta(weeks=1),
    "monthly": timedelta(days=30),
}


def _run_data_sources_sync() -> None:
    """Check all enabled data sources and sync those that are overdue."""
    logger.info("Scheduled data sources sync check starting...")
    db = SessionLocal()
    try:
        from app.models.data_source import DataSource
        from app.services.data_source_service import sync_source

        now = datetime.now(timezone.utc)
        sources = (
            db.query(DataSource)
            .filter(DataSource.is_enabled == True)  # noqa: E712
            .all()
        )
        synced = 0
        for ds in sources:
            freq = ds.sync_frequency
            if not freq or freq == "manual":
                continue
            interval = _FREQUENCY_INTERVALS.get(freq)
            if interval is None:
                continue
            last = ds.last_sync_at
            if last is None:
                # Never synced — run it now
                overdue = True
            else:
                # Make last timezone-aware if needed
                if last.tzinfo is None:
                    last = last.replace(tzinfo=timezone.utc)
                overdue = now - last >= interval
            if overdue:
                logger.info(
                    "Data source '%s' is overdue (freq=%s, last=%s) — syncing",
                    ds.name, freq, last,
                )
                try:
                    sync_source(db, str(ds.id))
                    synced += 1
                except Exception:
                    logger.exception("Failed to sync data source '%s'", ds.name)
        logger.info("Data sources sync check finished — %d source(s) synced", synced)
    except Exception:
        logger.exception("Data sources sync check failed")
    finally:
        db.close()


def _run_stale_detection() -> None:
    """Execute daily stale coverage detection inside its own DB session."""
    # Log info: "Scheduled stale coverage detection starting..."
    logger.info("Scheduled stale coverage detection starting...")
    # Assign db = SessionLocal()
    db = SessionLocal()
    # Attempt the following; catch errors below
    try:
        # Assign count = detect_stale_coverage(db)
        count = detect_stale_coverage(db)
        # Log info: "Stale detection finished — %d techniques flagged"
        logger.info("Stale detection finished — %d techniques flagged", count)
    # Handle Exception
    except Exception:
        # Log exception: "Stale coverage detection job failed"
        logger.exception("Stale coverage detection job failed")
    # Always execute this cleanup block
    finally:
        # Close the database session
        db.close()


def _run_decay_engine() -> None:
    """Execute the decay engine inside its own DB session."""
    logger.info("Scheduled decay engine job starting...")
    db = SessionLocal()
    try:
        from app.services.decay_engine_service import run_decay_engine
        results = run_decay_engine(db)
        logger.info("Decay engine job finished — %s", results)
    except Exception:
        logger.exception("Decay engine job failed")
    finally:
        db.close()


def _run_queue_generation() -> None:
    """Generate revalidation queue items for analysts — runs after decay engine."""
    logger.info("Scheduled revalidation queue generation starting...")
    db = SessionLocal()
    try:
        from app.services.revalidation_queue_service import generate_queue_items
        results = generate_queue_items(db)
        logger.info("Queue generation finished — %s", results)
    except Exception:
        logger.exception("Queue generation job failed")
    finally:
        db.close()


def _run_alert_evaluation() -> None:
    """Evaluate all enabled operational alert rules (hourly)."""
    logger.info("Scheduled alert evaluation job starting...")
    db = SessionLocal()
    try:
        from app.services.operational_alert_service import evaluate_all_rules
        result = evaluate_all_rules(db)
        logger.info(
            "Alert evaluation finished — %d rules, %d alerts fired in %.3fs",
            result["rules_evaluated"],
            result["alerts_fired"],
            result["duration_seconds"],
        )
    except Exception:
        logger.exception("Alert evaluation job failed")
    finally:
        db.close()


# ---------------------------------------------------------------------------
# Scheduler bootstrap
# ---------------------------------------------------------------------------


def start_scheduler() -> None:
    """Register all periodic jobs and start the background scheduler.

    Jobs registered:

    * ``mitre_sync`` — every **24 hours**
    * ``intel_scan`` — every **7 days**

    Neither job fires immediately on startup.
    """
    # Call scheduler.add_job()
    scheduler.add_job(
        _run_mitre_sync,
        # Keyword argument: trigger
        trigger="interval",
        # Keyword argument: hours
        hours=24,
        # Keyword argument: id
        id="mitre_sync",
        # Keyword argument: name
        name="MITRE ATT&CK sync (every 24h)",
        # Keyword argument: replace_existing
        replace_existing=True,
    )
    # Call scheduler.add_job()
    scheduler.add_job(
        _run_intel_scan,
        # Keyword argument: trigger
        trigger="interval",
        # Keyword argument: weeks
        weeks=1,
        # Keyword argument: id
        id="intel_scan",
        # Keyword argument: name
        name="Intel scan (every 7d)",
        # Keyword argument: replace_existing
        replace_existing=True,
    )
    # Call scheduler.add_job()
    scheduler.add_job(
        _run_notification_cleanup,
        # Keyword argument: trigger
        trigger="interval",
        # Keyword argument: hours
        hours=24,
        # Keyword argument: id
        id="notification_cleanup",
        # Keyword argument: name
        name="Notification cleanup (daily)",
        # Keyword argument: replace_existing
        replace_existing=True,
    )
    # Call scheduler.add_job()
    scheduler.add_job(
        _run_weekly_snapshot,
        # Keyword argument: trigger
        trigger="cron",
        # Keyword argument: day_of_week
        day_of_week="sun",
        # Keyword argument: hour
        hour=0,
        # Keyword argument: minute
        minute=0,
        # Keyword argument: id
        id="weekly_snapshot",
        # Keyword argument: name
        name="Weekly coverage snapshot (Sundays 00:00)",
        # Keyword argument: replace_existing
        replace_existing=True,
    )
    scheduler.add_job(
        _run_recurring_campaigns,
        # Keyword argument: trigger
        trigger="interval",
        # Keyword argument: hours
        hours=24,
        # Keyword argument: id
        id="recurring_campaigns",
        # Keyword argument: name
        name="Recurring campaigns check (daily)",
        # Keyword argument: replace_existing
        replace_existing=True,
    )
    # Call scheduler.add_job()
    scheduler.add_job(
        sync_all_jira_links,
        # Keyword argument: trigger
        trigger="interval",
        # Keyword argument: hours
        hours=1,
        # Keyword argument: id
        id="jira_sync",
        # Keyword argument: name
        name="Jira link sync (hourly)",
        # Keyword argument: replace_existing
        replace_existing=True,
    )
    # Call scheduler.add_job()
    scheduler.add_job(
        _run_osint_enrichment,
        # Keyword argument: trigger
        trigger="interval",
        # Keyword argument: weeks
        weeks=1,
        # Keyword argument: id
        id="osint_enrichment",
        # Keyword argument: name
        name="OSINT enrichment (weekly)",
        # Keyword argument: replace_existing
        replace_existing=True,
    )
    # Call scheduler.add_job()
    scheduler.add_job(
        _run_stale_detection,
        # Keyword argument: trigger
        trigger="interval",
        # Keyword argument: hours
        hours=24,
        # Keyword argument: id
        id="stale_detection",
        # Keyword argument: name
        name="Stale coverage detection (daily)",
        # Keyword argument: replace_existing
        replace_existing=True,
    )
    # Call scheduler.add_job()
    scheduler.add_job(
        run_retention_job,
        # Keyword argument: trigger
        trigger="interval",
        # Keyword argument: hours
        hours=24,
        # Keyword argument: id
        id="retention_policies",
        # Keyword argument: name
        name="Data retention policies (daily)",
        # Keyword argument: replace_existing
        replace_existing=True,
    )
    scheduler.add_job(
        _run_data_sources_sync,
        trigger="interval",
        hours=6,
        id="data_sources_sync",
        name="Data sources auto-sync (every 6h)",
        replace_existing=True,
    )
    scheduler.add_job(
        _run_decay_engine,
        trigger="cron",
        hour=2,
        minute=0,
        id="decay_engine",
        name="Detection decay engine (daily 02:00)",
        replace_existing=True,
    )
    scheduler.add_job(
        _run_queue_generation,
        trigger="cron",
        hour=2,
        minute=30,
        id="queue_generation",
        name="Revalidation queue generation (daily 02:30)",
        replace_existing=True,
    )
    scheduler.add_job(
        _run_alert_evaluation,
        trigger="interval",
        hours=1,
        id="alert_evaluation",
        name="Operational alert evaluation (hourly)",
        replace_existing=True,
    )
    scheduler.add_job(
        _run_evaluation_round_check,
        trigger="cron",
        day_of_week="mon",
        hour=6,
        minute=0,
        id="attck_evaluation_check",
        name="ATT&CK Evaluations new-round check (Mondays 06:00)",
        replace_existing=True,
    )
    scheduler.start()
    # Log info:
    logger.info(
        # Literal argument value
        "Background scheduler started — mitre_sync (24h), intel_scan (7d), "
        # Literal argument value
        "notification_cleanup (24h), weekly_snapshot (Sundays 00:00), "
        # Literal argument value
        "recurring_campaigns (daily), jira_sync (1h), "
        # Literal argument value
        "osint_enrichment (weekly), stale_detection (daily), "
        "retention_policies (daily), data_sources_sync (6h), "
        "alert_evaluation (1h), attck_evaluation_check (Mondays 06:00)"
    )
