"""Scheduler integration — run skills on a schedule with a daemon process.

No external dependencies — uses stdlib only (json, time, subprocess, signal).

Schedule storage: ~/.alive/schedules.json
Daemon PID: ~/.alive/daemon.pid
Daemon log: ~/.alive/logs/daemon.log

Usage:
    alive schedule "check bitcoin price" --every 30m
    alive schedule "summarize hacker news" --daily --at 08:00
    alive daemon start
    alive daemon stop
    alive schedules
"""

from __future__ import annotations

import json
import logging
import os
import re
import signal
import subprocess
import sys
import time
import uuid
from dataclasses import asdict, dataclass, field
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any

_log = logging.getLogger("alive")


# ── Data structures ─────────────────────────────────────────────────────────


@dataclass
class ScheduleTask:
    """A scheduled task definition."""

    id: str
    name: str
    request: str
    skill_name: str | None = None
    interval_seconds: int | None = None
    cron_expression: str | None = None
    time_of_day: str | None = None
    last_run_at: str | None = None
    next_run_at: str | None = None
    notify_via: str | None = None  # "telegram", "webhook:name", None
    last_result: str | None = None
    run_count: int = 0
    error_count: int = 0
    enabled: bool = True
    created_at: str = ""

    def __post_init__(self):
        if not self.created_at:
            self.created_at = datetime.now(timezone.utc).isoformat()
        if not self.id:
            self.id = str(uuid.uuid4())[:8]


# ── Schedule file I/O ──────────────────────────────────────────────────────


def _schedules_path() -> Path:
    """Return path to schedules.json."""
    from alive.config import ALIVE_HOME

    return ALIVE_HOME / "schedules.json"


def load_schedules() -> list[ScheduleTask]:
    """Load all scheduled tasks from disk."""
    path = _schedules_path()
    if not path.exists():
        return []
    try:
        data = json.loads(path.read_text())
        return [ScheduleTask(**t) for t in data]
    except (json.JSONDecodeError, TypeError, OSError) as e:
        _log.debug("Failed to load schedules: %s", e)
        return []


def save_schedules(tasks: list[ScheduleTask]) -> None:
    """Save all scheduled tasks to disk."""
    path = _schedules_path()
    path.parent.mkdir(parents=True, exist_ok=True)
    data = [asdict(t) for t in tasks]
    path.write_text(json.dumps(data, indent=2))


# ── Interval parsing ───────────────────────────────────────────────────────


def parse_interval(interval_str: str) -> int:
    """Parse a human-readable interval into seconds.

    Supports: 5m, 15m, 30m, 1h, 2h, 6h, 12h, 1d, 1w

    Args:
        interval_str: Interval string like "30m", "1h", "2d".

    Returns:
        Number of seconds.

    Raises:
        ValueError: If the interval string can't be parsed.
    """
    match = re.match(r"^(\d+)\s*([smhdw])$", interval_str.strip().lower())
    if not match:
        raise ValueError(
            f"Invalid interval: '{interval_str}'. "
            "Use format like: 5m, 30m, 1h, 2h, 6h, 12h, 1d, 1w"
        )

    amount = int(match.group(1))
    unit = match.group(2)

    multipliers = {"s": 1, "m": 60, "h": 3600, "d": 86400, "w": 604800}
    return amount * multipliers[unit]


def format_interval(seconds: int) -> str:
    """Format seconds as a human-readable interval string."""
    if seconds < 60:
        return f"{seconds}s"
    elif seconds < 3600:
        return f"{seconds // 60}m"
    elif seconds < 86400:
        return f"{seconds // 3600}h"
    elif seconds < 604800:
        return f"{seconds // 86400}d"
    else:
        return f"{seconds // 604800}w"


def calculate_next_run(task: ScheduleTask, from_time: datetime | None = None) -> str:
    """Calculate the next run time for a task.

    Returns ISO timestamp string.
    """
    now = from_time or datetime.now(timezone.utc)

    if task.interval_seconds:
        next_time = now + timedelta(seconds=task.interval_seconds)
        return next_time.isoformat()

    if task.time_of_day:
        # Parse HH:MM and calculate next occurrence
        try:
            hour, minute = map(int, task.time_of_day.split(":"))
            next_time = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
            if next_time <= now:
                next_time += timedelta(days=1)
            return next_time.isoformat()
        except (ValueError, AttributeError):
            pass

    if task.cron_expression:
        # Basic cron parsing for common patterns
        return _next_cron_run(task.cron_expression, now).isoformat()

    # Default: run in 1 hour
    return (now + timedelta(hours=1)).isoformat()


def _next_cron_run(cron: str, from_time: datetime) -> datetime:
    """Basic cron next-run calculator for common patterns.

    Supports: minute hour day-of-month month day-of-week
    """
    parts = cron.strip().split()
    if len(parts) != 5:
        return from_time + timedelta(hours=1)  # fallback

    minute, hour = parts[0], parts[1]

    # Simple case: fixed minute and hour (e.g., "0 9 * * *")
    try:
        m = int(minute) if minute != "*" else from_time.minute
        h = int(hour) if hour != "*" else from_time.hour
        next_time = from_time.replace(hour=h, minute=m, second=0, microsecond=0)
        if next_time <= from_time:
            next_time += timedelta(days=1)
        return next_time
    except ValueError:
        return from_time + timedelta(hours=1)


# ── Schedule management ────────────────────────────────────────────────────


def create_schedule(
    request: str,
    *,
    name: str | None = None,
    skill_name: str | None = None,
    interval: str | None = None,
    daily_at: str | None = None,
    cron: str | None = None,
    notify_via: str | None = None,
) -> ScheduleTask:
    """Create a new scheduled task.

    Args:
        request: Natural language request or skill name to run.
        name: Friendly name for the task (auto-generated if None).
        skill_name: Specific skill to run (optional).
        interval: Run interval like "30m", "1h", "2d".
        daily_at: Time of day like "08:00", "14:30".
        cron: Cron expression (5-field).
        notify_via: Notification channel ("telegram", "webhook:name").

    Returns:
        The created ScheduleTask.
    """
    # Auto-generate name from request
    if not name:
        words = re.findall(r"[a-z]+", request.lower())
        name = "-".join(words[:4]) or "scheduled-task"

    interval_seconds = None
    if interval:
        interval_seconds = parse_interval(interval)

    now = datetime.now(timezone.utc)

    task = ScheduleTask(
        id=str(uuid.uuid4())[:8],
        name=name,
        request=request,
        skill_name=skill_name,
        interval_seconds=interval_seconds,
        cron_expression=cron,
        time_of_day=daily_at,
        notify_via=notify_via,
        created_at=now.isoformat(),
    )

    # Calculate next run
    task.next_run_at = calculate_next_run(task, now)

    # Save
    tasks = load_schedules()
    tasks.append(task)
    save_schedules(tasks)

    return task


def remove_schedule(task_id_or_name: str) -> bool:
    """Remove a scheduled task by ID or name. Returns True if found and removed."""
    tasks = load_schedules()
    before = len(tasks)
    tasks = [
        t for t in tasks
        if t.id != task_id_or_name and t.name != task_id_or_name
    ]
    if len(tasks) < before:
        save_schedules(tasks)
        return True
    return False


def pause_schedule(task_id_or_name: str) -> bool:
    """Pause a scheduled task. Returns True if found."""
    tasks = load_schedules()
    for t in tasks:
        if t.id == task_id_or_name or t.name == task_id_or_name:
            t.enabled = False
            save_schedules(tasks)
            return True
    return False


def resume_schedule(task_id_or_name: str) -> bool:
    """Resume a paused scheduled task. Returns True if found."""
    tasks = load_schedules()
    for t in tasks:
        if t.id == task_id_or_name or t.name == task_id_or_name:
            t.enabled = True
            t.next_run_at = calculate_next_run(t)
            save_schedules(tasks)
            return True
    return False


# ── Daemon ─────────────────────────────────────────────────────────────────


def _pid_file() -> Path:
    """Return path to daemon.pid."""
    from alive.config import ALIVE_HOME

    return ALIVE_HOME / "daemon.pid"


def _log_file() -> Path:
    """Return path to daemon.log."""
    from alive.config import LOGS_DIR

    LOGS_DIR.mkdir(parents=True, exist_ok=True)
    return LOGS_DIR / "daemon.log"


def daemon_running() -> tuple[bool, int | None]:
    """Check if the daemon is running. Returns (running, pid)."""
    pid_file = _pid_file()
    if not pid_file.exists():
        return False, None
    try:
        pid = int(pid_file.read_text().strip())
        os.kill(pid, 0)  # Check if alive
        return True, pid
    except (ValueError, OSError, ProcessLookupError):
        return False, None


def start_daemon() -> int:
    """Start the scheduler daemon in the background.

    Returns:
        PID of the daemon process.

    Raises:
        RuntimeError: If daemon is already running.
    """
    running, pid = daemon_running()
    if running:
        raise RuntimeError(f"Daemon already running (PID {pid})")

    log_path = _log_file()
    pid_path = _pid_file()

    cmd = [sys.executable, "-m", "alive.integrations.scheduler", "--daemon"]

    with open(log_path, "a") as logf:
        proc = subprocess.Popen(
            cmd,
            stdout=logf,
            stderr=logf,
            start_new_session=True,
        )

    pid_path.parent.mkdir(parents=True, exist_ok=True)
    pid_path.write_text(str(proc.pid))
    return proc.pid


def stop_daemon() -> bool:
    """Stop the scheduler daemon. Returns True if stopped."""
    running, pid = daemon_running()
    if not running:
        return False

    try:
        os.kill(pid, signal.SIGTERM)
        _pid_file().unlink(missing_ok=True)
        return True
    except (OSError, ProcessLookupError):
        _pid_file().unlink(missing_ok=True)
        return False


def daemon_status() -> dict[str, Any]:
    """Get daemon status info."""
    running, pid = daemon_running()
    pid_file = _pid_file()

    info: dict[str, Any] = {
        "running": running,
        "pid": pid,
        "log_file": str(_log_file()),
    }

    if running and pid_file.exists():
        try:
            # Estimate uptime from PID file modification time
            mtime = pid_file.stat().st_mtime
            uptime = time.time() - mtime
            hours = int(uptime // 3600)
            minutes = int((uptime % 3600) // 60)
            info["uptime"] = f"{hours}h {minutes}m"
        except OSError:
            info["uptime"] = "unknown"

    tasks = load_schedules()
    info["total_tasks"] = len(tasks)
    info["active_tasks"] = sum(1 for t in tasks if t.enabled)
    info["total_runs"] = sum(t.run_count for t in tasks)
    info["total_errors"] = sum(t.error_count for t in tasks)

    # Next task
    active = [t for t in tasks if t.enabled and t.next_run_at]
    if active:
        active.sort(key=lambda t: t.next_run_at or "")
        info["next_task"] = active[0].name
        info["next_run"] = active[0].next_run_at

    return info


# ── Daemon main loop ──────────────────────────────────────────────────────


def _run_daemon_loop() -> None:
    """Main daemon loop — checks and runs scheduled tasks."""
    import logging

    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s %(levelname)s %(message)s",
    )
    log = logging.getLogger("alive.daemon")
    log.info("Alive scheduler daemon started (PID %d)", os.getpid())

    # Write our actual PID (might differ from parent's expectation in some cases)
    _pid_file().write_text(str(os.getpid()))

    running = True

    def handle_sigterm(signum, frame):
        nonlocal running
        log.info("Received SIGTERM, shutting down...")
        running = False

    signal.signal(signal.SIGTERM, handle_sigterm)

    while running:
        try:
            tasks = load_schedules()
            now = datetime.now(timezone.utc)

            for task in tasks:
                if not task.enabled:
                    continue
                if not task.next_run_at:
                    continue

                try:
                    next_run = datetime.fromisoformat(task.next_run_at)
                except (ValueError, TypeError):
                    continue

                if now >= next_run:
                    log.info("Running task: %s", task.name)
                    try:
                        result = _execute_task(task)
                        task.last_result = str(result)[:500]
                        task.run_count += 1
                        log.info("Task %s completed: %s", task.name, task.last_result[:100])

                        # Notify if configured
                        _notify_result(task, result)

                    except Exception as e:
                        task.error_count += 1
                        task.last_result = f"ERROR: {e}"
                        log.error("Task %s failed: %s", task.name, e)

                    task.last_run_at = now.isoformat()
                    task.next_run_at = calculate_next_run(task, now)

            save_schedules(tasks)

        except Exception as e:
            log.error("Daemon loop error: %s", e)

        # Sleep 30 seconds between checks
        for _ in range(30):
            if not running:
                break
            time.sleep(1)

    log.info("Daemon stopped.")
    _pid_file().unlink(missing_ok=True)


def _execute_task(task: ScheduleTask) -> str:
    """Execute a scheduled task and return the result string."""
    if task.skill_name:
        # Run a specific skill
        from alive.sandbox.runner import run_skill

        sr = run_skill(task.skill_name, {})
        if sr.success:
            return json.dumps(sr.output, default=str) if sr.output else "{}"
        else:
            raise RuntimeError(sr.error or "Skill execution failed")

    # Run through the normal pipeline (plan + compose)
    import asyncio

    from alive.core.composer import compose
    from alive.core.planner import plan_request

    async def _run():
        plan = await plan_request(task.request)
        result = await compose(plan)
        return result.final_output

    return asyncio.run(_run())


def _notify_result(task: ScheduleTask, result: str) -> None:
    """Send notification for task result if configured."""
    if not task.notify_via:
        return

    message = f"📋 {task.name}\n\n{result}"

    try:
        if task.notify_via == "telegram":
            from alive.integrations.telegram import send_telegram

            send_telegram(message)
        elif task.notify_via.startswith("webhook"):
            from alive.integrations.webhook import send_webhook

            parts = task.notify_via.split(":", 1)
            webhook_name = parts[1] if len(parts) > 1 else None
            send_webhook(message, webhook_name=webhook_name)
    except Exception as e:
        _log.error("Notification failed for %s: %s", task.name, e)


# ── Display ────────────────────────────────────────────────────────────────


def display_schedules(console=None) -> None:
    """Display all scheduled tasks as a rich table."""
    from rich.console import Console
    from rich.panel import Panel
    from rich.table import Table

    con = console or Console()
    tasks = load_schedules()

    if not tasks:
        con.print(Panel(
            "[dim]No scheduled tasks.[/]\n\n"
            "Create one:\n"
            "  alive schedule \"check something\" --every 30m",
            title="[bold]Scheduled Tasks[/]",
            border_style="yellow",
        ))
        return

    table = Table(title="Scheduled Tasks", border_style="cyan", show_lines=False)
    table.add_column("Task", style="bold green", no_wrap=True)
    table.add_column("Interval", style="cyan")
    table.add_column("Last Run", style="dim")
    table.add_column("Next", style="dim")
    table.add_column("Runs", justify="right")
    table.add_column("Status")

    for t in tasks:
        # Format interval
        if t.interval_seconds:
            interval = format_interval(t.interval_seconds)
        elif t.time_of_day:
            interval = f"daily {t.time_of_day}"
        elif t.cron_expression:
            interval = f"cron"
        else:
            interval = "—"

        # Format last run
        if t.last_run_at:
            try:
                dt = datetime.fromisoformat(t.last_run_at)
                last_run = dt.strftime("%H:%M")
            except ValueError:
                last_run = "?"
        else:
            last_run = "never"

        # Format next run
        if t.next_run_at:
            try:
                dt = datetime.fromisoformat(t.next_run_at)
                now = datetime.now(timezone.utc)
                delta = dt - now
                if delta.total_seconds() < 60:
                    next_run = "now"
                elif delta.total_seconds() < 3600:
                    next_run = f"{int(delta.total_seconds() // 60)}m"
                else:
                    next_run = dt.strftime("%H:%M")
            except ValueError:
                next_run = "?"
        else:
            next_run = "—"

        status = "[green]✅ on[/]" if t.enabled else "[dim]⏸ off[/]"

        runs = str(t.run_count)
        if t.error_count:
            runs += f" [red]({t.error_count} err)[/]"

        table.add_row(t.name, interval, last_run, next_run, runs, status)

    con.print(table)

    # Daemon status footer
    running, pid = daemon_running()
    con.print()
    if running:
        con.print(f"[dim]Daemon: running (PID {pid})[/]")
    else:
        con.print("[dim]Daemon: not running — start with: alive daemon start[/]")


def display_daemon_status(console=None) -> None:
    """Display daemon status as a rich panel."""
    from rich.console import Console
    from rich.panel import Panel

    con = console or Console()
    status = daemon_status()

    if status["running"]:
        lines = [
            f"Status: [green]✅ Running[/] (PID {status['pid']})",
            f"Uptime: {status.get('uptime', 'unknown')}",
            f"Tasks: {status['active_tasks']} active / {status['total_tasks']} total",
            f"Runs today: {status['total_runs']}",
        ]
        if status.get("total_errors"):
            lines.append(f"Errors: [red]{status['total_errors']}[/]")
        if status.get("next_task"):
            lines.append(f"Next: {status['next_task']} at {status.get('next_run', '?')}")
        lines.append(f"Log: [dim]{status['log_file']}[/]")
    else:
        lines = [
            "Status: [dim]⏹ Not running[/]",
            "",
            "[dim]Start with: alive daemon start[/]",
        ]

    con.print(Panel(
        "\n".join(lines),
        title="[bold cyan]Daemon Status[/]",
        border_style="cyan",
        padding=(1, 2),
    ))


# ── CLI entry point ────────────────────────────────────────────────────────


if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser()
    parser.add_argument("--daemon", action="store_true")
    args = parser.parse_args()

    if args.daemon:
        _run_daemon_loop()
