"""
agentpaas.services.scheduler — Persistent task scheduling

Supports cron expressions, intervals, and one-time schedules.
Tasks persist across restarts via JSON file.
"""
from __future__ import annotations

import json
import os
import time
import uuid
import threading
from dataclasses import dataclass, field, asdict
from typing import Any, Callable, Dict, List, Optional


@dataclass
class ScheduledTask:
    id: str = ""
    schedule: str = ""          # cron expr or interval ("5m", "1h")
    agent_id: str = ""
    input: str = ""
    notify_channel: str = "terminal"
    recurring: bool = True
    enabled: bool = True
    created_at: float = 0
    last_run: float = 0
    next_run: float = 0
    run_count: int = 0
    max_runs: int = 0           # 0 = unlimited
    description: str = ""

    def __post_init__(self):
        if not self.id:
            self.id = f"sched_{uuid.uuid4().hex[:8]}"
        if self.created_at == 0:
            self.created_at = time.time()
        if self.next_run == 0:
            self.next_run = self._calc_next_run()

    def _calc_next_run(self) -> float:
        """Calculate next run time from schedule string."""
        now = time.time()
        s = self.schedule.strip()
        # Interval format: 5m, 1h, 30s, 1d
        if s and s[-1] in "smhd" and s[:-1].isdigit():
            multipliers = {"s": 1, "m": 60, "h": 3600, "d": 86400}
            interval = int(s[:-1]) * multipliers[s[-1]]
            base = self.last_run if self.last_run > 0 else now
            return base + interval
        # Default: 1 hour
        return now + 3600

    def is_due(self) -> bool:
        return self.enabled and time.time() >= self.next_run

    def mark_run(self):
        self.last_run = time.time()
        self.run_count += 1
        if not self.recurring or (self.max_runs > 0 and self.run_count >= self.max_runs):
            self.enabled = False
        else:
            self.next_run = self._calc_next_run()


class Scheduler:
    """Persistent task scheduler."""

    def __init__(self, persist_path: str = ""):
        self.persist_path = persist_path or os.path.expanduser("~/.lambdagent/schedules.json")
        self._tasks: Dict[str, ScheduledTask] = {}
        self._callbacks: Dict[str, Callable] = {}
        self._lock = threading.Lock()
        self._load()

    def create(self, schedule: str, agent_id: str = "", input: str = "",
               description: str = "", recurring: bool = True,
               notify_channel: str = "terminal", max_runs: int = 0) -> ScheduledTask:
        task = ScheduledTask(
            schedule=schedule, agent_id=agent_id, input=input,
            description=description, recurring=recurring,
            notify_channel=notify_channel, max_runs=max_runs,
        )
        with self._lock:
            self._tasks[task.id] = task
        self._save()
        return task

    def delete(self, task_id: str) -> bool:
        with self._lock:
            if task_id in self._tasks:
                del self._tasks[task_id]
                self._save()
                return True
        return False

    def pause(self, task_id: str) -> bool:
        with self._lock:
            task = self._tasks.get(task_id)
            if task:
                task.enabled = not task.enabled
                self._save()
                return True
        return False

    def list_all(self) -> List[ScheduledTask]:
        with self._lock:
            return list(self._tasks.values())

    def get(self, task_id: str) -> Optional[ScheduledTask]:
        return self._tasks.get(task_id)

    def check_due(self) -> List[ScheduledTask]:
        """Get all tasks that are due for execution."""
        due = []
        with self._lock:
            for task in self._tasks.values():
                if task.is_due():
                    due.append(task)
        return due

    def mark_executed(self, task_id: str):
        with self._lock:
            task = self._tasks.get(task_id)
            if task:
                task.mark_run()
                if not task.enabled:
                    del self._tasks[task_id]
                self._save()

    def _save(self):
        os.makedirs(os.path.dirname(self.persist_path), exist_ok=True)
        data = {tid: asdict(t) for tid, t in self._tasks.items()}
        with open(self.persist_path, "w") as f:
            json.dump(data, f, indent=2)

    def _load(self):
        if not os.path.exists(self.persist_path):
            return
        try:
            with open(self.persist_path) as f:
                data = json.load(f)
            for tid, td in data.items():
                self._tasks[tid] = ScheduledTask(**{k: v for k, v in td.items()
                                                     if k in ScheduledTask.__dataclass_fields__})
        except Exception:
            pass


# ════════════════════════════════════════════════════════════
# Agent-side tool functions
# ════════════════════════════════════════════════════════════

_default_scheduler = None

def _get_scheduler() -> Scheduler:
    global _default_scheduler
    if _default_scheduler is None:
        _default_scheduler = Scheduler()
    return _default_scheduler


def schedule_create(input_val: Any) -> str:
    params = _parse(input_val)
    schedule = params.get("schedule", "")
    if not schedule:
        return "[ERROR] schedule is required (e.g. '5m', '1h', '1d')"

    task = _get_scheduler().create(
        schedule=schedule,
        input=params.get("input", params.get("prompt", "")),
        description=params.get("description", ""),
        recurring=params.get("recurring", True),
        notify_channel=params.get("notify", "terminal"),
    )

    return f"[OK] Schedule created: {task.id} ({task.schedule}, {'recurring' if task.recurring else 'once'})"


def schedule_list(input_val: Any) -> str:
    tasks = _get_scheduler().list_all()
    if not tasks:
        return "[EMPTY] No scheduled tasks"
    lines = ["Scheduled tasks:"]
    for t in tasks:
        status = "✅" if t.enabled else "⏸"
        age = f"runs={t.run_count}" if t.run_count > 0 else "pending"
        lines.append(f"  {status} {t.id}: {t.schedule} — {t.description or t.input[:50]} ({age})")
    return "\n".join(lines)


def schedule_delete(input_val: Any) -> str:
    params = _parse(input_val)
    task_id = params.get("id", params.get("task_id", ""))
    if not task_id:
        return "[ERROR] id is required"
    if _get_scheduler().delete(task_id):
        return f"[OK] Schedule deleted: {task_id}"
    return f"[ERROR] Schedule not found: {task_id}"


def _parse(input_val: Any) -> dict:
    if isinstance(input_val, dict):
        return input_val
    if isinstance(input_val, str):
        try:
            return json.loads(input_val)
        except (json.JSONDecodeError, ValueError):
            return {"schedule": input_val}
    return {}
