"""
BizNode – Digital Business Operator
Copyright 2026 1BZ DZIT DAO LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

---

Background Scheduler
====================
APScheduler-backed cron/interval task runner that loads tasks from the
`scheduled_tasks` DB table on startup and dispatches to:
  - http_call      → requests.request(...)
  - mcp_command    → POST <peer_endpoint>/api/execute with MCP token
  - agent_goal     → existing agent loop
  - python_eval    → restricted math/string eval
"""

import json
import logging
import importlib.util
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional

from apscheduler.schedulers.background import BackgroundScheduler as _APScheduler
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.interval import IntervalTrigger

log = logging.getLogger("biznode.scheduler")

# ─── DB loader (bypass memory/__init__.py) ───────────────────────────────────

def _load_db():
    db_path = Path(__file__).parent.parent / "memory" / "database.py"
    spec = importlib.util.spec_from_file_location("_db", db_path)
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod


# ─── Action dispatcher ───────────────────────────────────────────────────────

def execute_action(action: dict) -> dict:
    """
    Dispatch a task action and return a result dict.

    Supported types:
      http_call     { method, url, headers, body }
      mcp_command   { peer_endpoint, token, task_id, params }
      agent_goal    { goal, user_id, context }
      python_eval   { expression }   — restricted, no imports
    """
    action_type = action.get("type", "agent_goal")

    if action_type == "http_call":
        import requests
        method = action.get("method", "GET").upper()
        url = action.get("url", "")
        headers = action.get("headers", {})
        body = action.get("body")
        resp = requests.request(method, url, headers=headers, json=body, timeout=30)
        return {"status_code": resp.status_code, "body": resp.text[:4096]}

    elif action_type == "mcp_command":
        import requests
        peer_endpoint = action.get("peer_endpoint", "").rstrip("/")
        token = action.get("token", "")
        task_id = action.get("task_id")
        params = action.get("params", {})
        resp = requests.post(
            f"{peer_endpoint}/api/execute",
            headers={"Authorization": f"MCP {token}"},
            json={"task_id": task_id, "params": params},
            timeout=30,
        )
        return {"status_code": resp.status_code, "body": resp.json()}

    elif action_type == "agent_goal":
        try:
            from automation.agent_loop import run_agent_goal
            goal = action.get("goal", "")
            user_id = action.get("user_id", "scheduler")
            context = action.get("context", {})
            return run_agent_goal(user_id=user_id, goal=goal, context=context)
        except ImportError:
            return {"error": "agent_loop not available"}

    elif action_type == "python_eval":
        expression = action.get("expression", "")
        # Restricted: only safe builtins; no imports, no exec
        safe_builtins = {k: __builtins__[k] for k in (
            "abs", "all", "any", "bool", "divmod", "float", "int",
            "len", "list", "max", "min", "pow", "range", "round",
            "sorted", "str", "sum", "tuple", "zip",
        ) if k in (
            __builtins__ if isinstance(__builtins__, dict)
            else dir(__builtins__)
        )}
        import math
        result = eval(expression, {"__builtins__": safe_builtins, "math": math}, {})  # noqa: S307
        return {"result": result}

    else:
        return {"error": f"Unknown action type: {action_type}"}


# ─── Per-task job function ────────────────────────────────────────────────────

def _run_task(task_id: str):
    """Called by APScheduler for each due task."""
    db = _load_db()
    task = db.get_scheduled_task(task_id)
    if not task or task["status"] != "active":
        return

    log.info("SCHEDULE_TRIGGERED task_id=%s name=%s", task_id, task.get("name"))

    try:
        action = json.loads(task["action_json"])
        execute_action(action)
    except Exception as exc:
        log.error("Task %s failed: %s", task_id, exc)

    now = datetime.now(timezone.utc).isoformat()
    try:
        db.record_task_run(task_id, last_run=now)
    except Exception:
        pass

    # Emit to audit log if available
    try:
        from services.audit_logger import log_event
        log_event("SCHEDULE_TRIGGERED", {"task_id": task_id, "name": task.get("name")})
    except Exception:
        pass


# ─── Scheduler wrapper ───────────────────────────────────────────────────────

class BizNodeScheduler:
    """
    Thin wrapper around APScheduler's BackgroundScheduler.
    Loads all active DB tasks on start() and keeps APScheduler jobs in sync.
    """

    def __init__(self):
        self._ap = _APScheduler(timezone="UTC")
        self._started = False

    def start(self):
        if self._started:
            return
        self._ap.start()
        self._started = True
        self._load_db_tasks()
        self._register_backup_jobs()
        self._register_marketing_jobs()
        self._register_campaign_jobs()
        self._register_node_health_check()
        self._register_daily_bot_reports()
        log.info("BizNode scheduler started")

    def stop(self):
        if self._started:
            self._ap.shutdown(wait=False)
            self._started = False

    # ── DB-backed task management ────────────────────────────────────────────

    def _load_db_tasks(self):
        """Register all active DB tasks with APScheduler."""
        try:
            db = _load_db()
            tasks = db.get_scheduled_tasks(status="active")
            for task in tasks:
                self._register_job(task)
            log.info("Loaded %d scheduled tasks from DB", len(tasks))
        except Exception as exc:
            log.warning("Could not load DB tasks: %s", exc)

    def _register_backup_jobs(self):
        """Register a single consolidated backup cron job if any provider is enabled."""
        try:
            db = _load_db()
            configs = [c for c in db.get_all_backup_configs() if c["enabled"]]
            if not configs:
                return
            cron = configs[0].get("schedule_cron") or "0 2 * * *"
            self._ap.add_job(
                _run_backup_job,
                CronTrigger.from_crontab(cron, timezone="UTC"),
                id="biznode_backup",
                replace_existing=True,
                misfire_grace_time=300,
            )
            log.info("Backup job registered with cron: %s", cron)
        except Exception as exc:
            log.warning("Could not register backup job: %s", exc)

    def reschedule_backup(self, cron: str):
        """Update the backup cron schedule (called after config change)."""
        try:
            self._ap.add_job(
                _run_backup_job,
                CronTrigger.from_crontab(cron, timezone="UTC"),
                id="biznode_backup",
                replace_existing=True,
                misfire_grace_time=300,
            )
            log.info("Backup job rescheduled: %s", cron)
        except Exception as exc:
            log.warning("Could not reschedule backup job: %s", exc)

    def _register_marketing_jobs(self):
        """Register daily digest cron + IMAP poll interval jobs."""
        try:
            import os as _os
            digest_cron = _os.getenv("DIGEST_CRON", "0 22 * * *")
            self._ap.add_job(
                _run_digest_job,
                CronTrigger.from_crontab(digest_cron, timezone="UTC"),
                id="biznode_digest",
                replace_existing=True,
                misfire_grace_time=300,
            )
            self._ap.add_job(
                _run_imap_check,
                IntervalTrigger(minutes=15),
                id="biznode_imap",
                replace_existing=True,
                misfire_grace_time=60,
            )
            log.info("Marketing jobs registered (digest: %s, IMAP: every 15 min)", digest_cron)
        except Exception as exc:
            log.warning("Could not register marketing jobs: %s", exc)

        # Daily promotional email job
        try:
            import os as _os
            promo_cron = _os.getenv("PROMO_EMAIL_CRON", "0 8 * * *")
            self._ap.add_job(
                _run_promo_email_job,
                CronTrigger.from_crontab(promo_cron, timezone="UTC"),
                id="biznode_promo_email",
                replace_existing=True,
                misfire_grace_time=300,
            )
            log.info("Promo email job registered (cron: %s)", promo_cron)
        except Exception as exc:
            log.warning("Could not register promo email job: %s", exc)

        # Video task processor — check for pending tasks every 5 minutes
        try:
            self._ap.add_job(
                _run_video_task_check,
                IntervalTrigger(minutes=5),
                id="biznode_video_tasks",
                replace_existing=True,
                misfire_grace_time=120,
            )
            log.info("Video task processor registered (every 5 min)")
        except Exception as exc:
            log.warning("Could not register video task processor: %s", exc)

        # Daily customer engagement emails
        try:
            import os as _os
            engagement_cron = _os.getenv("ENGAGEMENT_CRON", "0 9 * * *")
            self._ap.add_job(
                _run_engagement_job,
                CronTrigger.from_crontab(engagement_cron, timezone="UTC"),
                id="biznode_engagement",
                replace_existing=True,
                misfire_grace_time=300,
            )
            log.info("Engagement email job registered (cron: %s)", engagement_cron)
        except Exception as exc:
            log.warning("Could not register engagement job: %s", exc)

        # Group advertisement sender — every 6 hours
        try:
            self._ap.add_job(
                _run_group_ads_job,
                IntervalTrigger(hours=6),
                id="biznode_group_ads",
                replace_existing=True,
                misfire_grace_time=300,
            )
            log.info("Group ads job registered (every 6 hours)")
        except Exception as exc:
            log.warning("Could not register group ads job: %s", exc)

        # Webhook sync — poll VPS for unprocessed payment events every 2 min
        try:
            self._ap.add_job(
                _run_webhook_sync,
                IntervalTrigger(minutes=2),
                id="biznode_webhook_sync",
                replace_existing=True,
                misfire_grace_time=60,
            )
            log.info("Webhook sync job registered (every 2 min)")
        except Exception as exc:
            log.warning("Could not register webhook sync job: %s", exc)

    def _register_campaign_jobs(self):
        """Register marketing campaign automation jobs."""
        try:
            from marketingteam.campaign_scheduler import register_campaign_jobs
            register_campaign_jobs(self._ap)
        except Exception as exc:
            log.warning("Could not register campaign jobs: %s", exc)

    def _register_node_health_check(self):
        """Check remote basic/pro nodes every hour."""
        try:
            self._ap.add_job(
                _run_node_health_check,
                IntervalTrigger(hours=1),
                id="biznode_node_health",
                replace_existing=True,
                misfire_grace_time=300,
            )
            log.info("Node health check registered (every 1 hour)")
        except Exception as exc:
            log.warning("Could not register node health check: %s", exc)

    def _register_daily_bot_reports(self):
        """Register daily hosted bot reports job at 23:00 UTC."""
        try:
            self._ap.add_job(
                _daily_bot_reports,
                CronTrigger.from_crontab("0 23 * * *", timezone="UTC"),
                id="biznode_daily_bot_reports",
                replace_existing=True,
                misfire_grace_time=300,
            )
            log.info("Daily bot reports job registered (23:00 UTC)")
        except Exception as exc:
            log.warning("Could not register daily bot reports job: %s", exc)

    def _register_job(self, task: dict):
        """Add or replace an APScheduler job for this task record."""
        task_id = task["id"]
        job_id = f"task_{task_id}"

        if task.get("schedule_cron"):
            trigger = CronTrigger.from_crontab(task["schedule_cron"], timezone="UTC")
        elif task.get("interval_seconds"):
            trigger = IntervalTrigger(seconds=int(task["interval_seconds"]))
        else:
            log.warning("Task %s has no schedule — skipping", task_id)
            return

        self._ap.add_job(
            _run_task,
            trigger=trigger,
            id=job_id,
            args=[task_id],
            replace_existing=True,
            misfire_grace_time=120,
        )

    def add_task(self, task: dict):
        """Register a newly-created task without restarting the scheduler."""
        if self._started:
            self._register_job(task)

    def remove_task(self, task_id: str):
        """Remove an APScheduler job for a deleted/paused task."""
        job_id = f"task_{task_id}"
        if self._ap.get_job(job_id):
            self._ap.remove_job(job_id)

    def run_now(self, task_id: str) -> dict:
        """Manually trigger a task immediately (synchronous)."""
        db = _load_db()
        task = db.get_scheduled_task(task_id)
        if not task:
            return {"error": "Task not found"}
        try:
            action = json.loads(task["action_json"])
            result = execute_action(action)
            now = datetime.now(timezone.utc).isoformat()
            db.record_task_run(task_id, last_run=now)
            return result
        except Exception as exc:
            return {"error": str(exc)}


# ─── Module-level singleton ───────────────────────────────────────────────────

_scheduler: Optional[BizNodeScheduler] = None


def _run_backup_job():
    """Module-level function called by APScheduler for backup jobs."""
    try:
        from services.backup_service import run_backup
        result = run_backup()
        log.info("Scheduled backup completed: %s", result)
    except Exception as exc:
        log.error("Scheduled backup job failed: %s", exc)


def _run_digest_job():
    """Module-level function called by APScheduler for daily digest emails."""
    try:
        from services.digest_service import send_daily_digest
        send_daily_digest()
    except Exception as exc:
        log.error("Scheduled digest job failed: %s", exc)


def _run_imap_check():
    """Module-level function called by APScheduler for IMAP inbox polling."""
    try:
        from services.email_inbox_service import check_email_instructions
        result = check_email_instructions()
        log.info("IMAP check completed: processed=%s", result.get("processed", 0))
    except Exception as exc:
        log.error("Scheduled IMAP check failed: %s", exc)


def _run_promo_email_job():
    """Module-level function called by APScheduler for daily promotional emails."""
    import os as _os
    # Scheduler runs inside the dashboard process — always use localhost
    _os.environ.setdefault("PROMO_DASHBOARD_URL", "http://localhost:7777")
    try:
        from services.promo_email_service import run_promo_emails
        result = run_promo_emails(dashboard_url="http://localhost:7777")
        log.info("Promo emails: sent=%s skipped=%s errors=%s",
                 result.get("sent", 0), result.get("skipped", 0), result.get("errors", 0))
    except Exception as exc:
        log.error("Promo email job failed: %s", exc)


def _run_video_task_check():
    """Module-level function called by APScheduler to process pending video tasks."""
    try:
        from videoautomation.pipeline import process_pending_tasks
        result = process_pending_tasks()
        if result.get("processed", 0) > 0:
            log.info("Video task check: processed=%s", result["processed"])
    except ImportError:
        pass  # video automation not installed
    except Exception as exc:
        log.error("Video task check failed: %s", exc)


def _run_engagement_job():
    """Module-level function called by APScheduler for daily customer engagement."""
    try:
        from services.customer_engagement_service import run_daily_engagement
        result = run_daily_engagement(dashboard_url="http://localhost:7777")
        log.info("Engagement emails: sent=%s skipped=%s errors=%s",
                 result.get("sent", 0), result.get("skipped", 0), result.get("errors", 0))
    except Exception as exc:
        log.error("Engagement email job failed: %s", exc)


def _run_group_ads_job():
    """Module-level function called by APScheduler to send group advertisements."""
    try:
        from services.group_ad_service import get_pending_ads, mark_ad_sent
        ads = get_pending_ads()
        if not ads:
            return
        # Group ads are prepared here; actual Telegram send needs bot context
        # We store them as pending and the bot picks them up
        log.info("Group ads: %d ads pending for delivery", len(ads))
    except Exception as exc:
        log.error("Group ads job failed: %s", exc)


def _run_webhook_sync():
    """Module-level function called by APScheduler to sync VPS webhook events."""
    try:
        from services.webhook_sync_service import sync_webhook_events
        result = sync_webhook_events()
        if result.get("processed", 0) > 0 or result.get("failed", 0) > 0:
            log.info("Webhook sync: processed=%s failed=%s",
                     result.get("processed", 0), result.get("failed", 0))
    except ImportError:
        pass  # webhook sync service not available
    except Exception as exc:
        log.error("Webhook sync job failed: %s", exc)


def _run_node_health_check():
    """Check remote BizNode nodes are alive and alert owner if down."""
    import urllib.request
    import urllib.error

    NODES = [
        {
            "id": "basic-asuszen",
            "label": "Basic (ASUSZEN)",
            "ip": "192.168.1.106",
            "port": 7877,
            "tier": "basic",
        },
        {
            "id": "pro-asuszen",
            "label": "Pro (ASUSZEN)",
            "ip": "192.168.1.106",
            "port": 7977,
            "tier": "pro",
        },
    ]

    NODEMONITOR_URL = "http://1bz.biz:7333"
    NODEMONITOR_KEY = "Nk2xGrJ4ZyERwBa3mCH33iF_CwgWh6uRMFjKvcc8e48"

    down_nodes = []
    for node in NODES:
        url = f"http://{node['ip']}:{node['port']}/api/health"
        try:
            req = urllib.request.Request(url, method="GET")
            with urllib.request.urlopen(req, timeout=10) as resp:
                if resp.status == 200:
                    log.info("Node %s (%s:%s) is UP", node["id"], node["ip"], node["port"])
                    continue
        except Exception:
            pass
        log.warning("Node %s (%s:%s) is DOWN", node["id"], node["ip"], node["port"])
        down_nodes.append(node)

    if not down_nodes:
        return

    # Alert owner via Telegram
    import os as _os
    bot_token = _os.getenv("BOT_TOKEN") or _os.getenv("TELEGRAM_BOT_TOKEN", "")
    owner_id = _os.getenv("OWNER_TELEGRAM_ID", "")
    if not bot_token or not owner_id:
        return

    lines = ["*Node Health Alert*\n"]
    for node in down_nodes:
        lines.append(
            f"*{node['label']}* ({node['id']}) — DOWN\n"
            f"  Address: {node['ip']}:{node['port']}"
        )
    lines.append(f"\nChecked at: {__import__('datetime').datetime.now().strftime('%H:%M:%S')}")
    msg = "\n".join(lines)

    try:
        import json as _json
        payload = _json.dumps({"chat_id": owner_id, "text": msg, "parse_mode": "Markdown"}).encode()
        req = urllib.request.Request(
            f"https://api.telegram.org/bot{bot_token}/sendMessage",
            data=payload,
            headers={"Content-Type": "application/json"},
            method="POST",
        )
        urllib.request.urlopen(req, timeout=10)
        log.info("Sent node health alert to owner")
    except Exception as exc:
        log.error("Failed to send node health alert: %s", exc)

    # Also report to nodemonitor
    try:
        for node in down_nodes:
            payload = _json.dumps({
                "node_id": node["id"],
                "status": "down",
                "checked_by": "admin-scheduler",
            }).encode()
            req = urllib.request.Request(
                f"{NODEMONITOR_URL}/report",
                data=payload,
                headers={
                    "Content-Type": "application/json",
                    "x-api-key": NODEMONITOR_KEY,
                },
                method="POST",
            )
            urllib.request.urlopen(req, timeout=10)
    except Exception:
        pass


def _daily_bot_reports():
    """Generate and email daily reports for all hosted bots + personalized user emails."""
    try:
        from services.hosted_bot_runner import HostedBotRunner
        runner = HostedBotRunner()
        result = runner.generate_daily_reports()
        log.info("Daily bot reports: %s", result)
        # Send personalized emails to opted-in users
        user_result = runner.send_personalized_user_emails()
        log.info("Personalized user emails: %s", user_result)
    except Exception as exc:
        log.error("Daily bot reports failed: %s", exc)


def get_scheduler() -> BizNodeScheduler:
    """Return the module-level scheduler singleton (creates it if needed)."""
    global _scheduler
    if _scheduler is None:
        _scheduler = BizNodeScheduler()
    return _scheduler


def start_scheduler():
    get_scheduler().start()


def stop_scheduler():
    get_scheduler().stop()
