# tools/schedule_tools.py — User-facing tools for scheduling autonomous tasks.
# Tasks are persisted in chat_storage.scheduled_jobs and executed by cron_scheduler.py.
from __future__ import annotations

import json
import re
from datetime import datetime, timedelta
from uuid import uuid4

from langchain_core.tools import tool
from pydantic import BaseModel, Field
from tools._logging import _debug_log, _emit_input_status


def _parse_schedule(schedule_str: str, timezone: str = "") -> tuple[datetime | None, str]:
    """Parse a human-readable schedule into a next_run_at datetime.

    Supports:
      - ISO 8601: '2026-03-10T14:00:00'
      - Relative: 'in 30 minutes', 'in 2 hours', 'in 1 day'
      - Daily: 'daily at 9:00', 'daily at 14:30'
      - Simple: 'tomorrow at 10:00', 'tomorrow'
      - Cron: '*/5 * * * *', '0 9 * * MON-FRI'

    Returns (next_run_datetime, error_message).
    """
    s = schedule_str.strip()
    now = datetime.now()

    # Cron expression (5-part)
    if re.match(r"^[\d\*/,\-]+\s+[\d\*/,\-]+\s+[\d\*/,\-]+\s+[\d\*/,\-]+\s+[\d\*/,\-A-Za-z]+$", s):
        try:
            from croniter import croniter
            if timezone:
                try:
                    from zoneinfo import ZoneInfo
                    tz = ZoneInfo(timezone)
                    now_in_tz = datetime.now(tz)
                    next_aware = croniter(s, now_in_tz).get_next(datetime)
                    if next_aware.tzinfo is None:
                        next_aware = next_aware.replace(tzinfo=tz)
                    return next_aware.astimezone().replace(tzinfo=None), ""
                except Exception:
                    pass
            cron = croniter(s, now)
            return cron.get_next(datetime), ""
        except Exception as e:
            return None, f"Invalid cron expression '{s}': {e}"

    s_lower = s.lower()

    # ISO 8601
    try:
        dt = datetime.fromisoformat(schedule_str.strip())
        return dt, ""
    except (ValueError, TypeError):
        pass

    # Relative: "in N minutes/hours/days"
    rel = re.match(r"in\s+(\d+)\s+(minute|hour|day|min|hr)s?", s_lower)
    if rel:
        n = int(rel.group(1))
        unit = rel.group(2)
        if unit in ("minute", "min"):
            return now + timedelta(minutes=n), ""
        elif unit in ("hour", "hr"):
            return now + timedelta(hours=n), ""
        elif unit == "day":
            return now + timedelta(days=n), ""

    # Daily at HH:MM
    daily = re.match(r"daily\s+at\s+(\d{1,2}):(\d{2})", s_lower)
    if daily:
        h, m = int(daily.group(1)), int(daily.group(2))
        next_dt = now.replace(hour=h, minute=m, second=0, microsecond=0)
        if next_dt <= now:
            next_dt += timedelta(days=1)
        return next_dt, ""

    # Tomorrow at HH:MM
    tom = re.match(r"tomorrow(?:\s+at\s+(\d{1,2}):(\d{2}))?", s_lower)
    if tom:
        h = int(tom.group(1) or 9)
        m = int(tom.group(2) or 0)
        next_dt = (now + timedelta(days=1)).replace(hour=h, minute=m, second=0, microsecond=0)
        return next_dt, ""

    return None, (
        f"Could not parse schedule '{schedule_str}'. Use ISO 8601, 'in N minutes/hours/days', "
        "'daily at HH:MM', 'tomorrow at HH:MM', or a cron expression like '*/5 * * * *'."
    )


class ScheduleTaskInput(BaseModel):
    prompt: str = Field(description="Task description / instructions for the agent to execute at the scheduled time.")
    schedule: str = Field(description="When to run: ISO 8601, 'in N minutes/hours/days', 'daily at HH:MM', 'tomorrow at HH:MM', or cron expression like '*/5 * * * *'.")
    repeat_count: int = Field(default=1, ge=1, le=365, description="How many times to run (1 = one-shot, >1 = recurring). Max 365.")
    delivery: str = Field(default="origin", description="Where to deliver results: 'origin' (same thread), 'file' (save to workspace), 'notification' (macOS notification), or 'webhook' (POST to URL).")
    timezone: str = Field(default="", description="IANA timezone for the schedule (e.g. 'America/New_York'). Empty uses system local time.")
    model_override: str = Field(default="", description="Optional model to use for this job (must be in the allowed models list).")
    webhook_url: str = Field(default="", description="Webhook URL for 'webhook' delivery (must be in config allowlist).")


def _validate_model_override(model: str) -> str | None:
    """Validate model_override against config allowlist. Returns error message or None."""
    if not model:
        return None
    try:
        from core.config import get_config
        cfg = get_config()
        allowed = (cfg.get("scheduling") or {}).get("allowed_models", [])
        if not allowed:
            return None
        if model not in allowed:
            return f"Model '{model}' not in allowed list: {allowed}"
    except Exception:
        pass
    return None


@tool(args_schema=ScheduleTaskInput)
def schedule_task(
    prompt: str,
    schedule: str,
    repeat_count: int = 1,
    delivery: str = "origin",
    timezone: str = "",
    model_override: str = "",
    webhook_url: str = "",
) -> str:
    """Schedule a task to run autonomously at a future time. The task will execute in a fresh
    agent session without personality. Use for reminders, periodic checks, data fetches, or
    any work that should happen later without the user present."""
    _debug_log(f"[TOOL] schedule_task called: schedule={schedule}", "TOOL")
    _emit_input_status("Scheduling task…")

    if model_override:
        model_err = _validate_model_override(model_override)
        if model_err:
            _emit_input_status("")
            return model_err

    next_run, err = _parse_schedule(schedule, timezone=timezone)
    if err:
        _emit_input_status("")
        return err

    if not prompt.strip():
        _emit_input_status("")
        return "Task prompt cannot be empty."

    delivery = delivery.strip().lower()
    if delivery not in ("origin", "file", "notification", "webhook"):
        delivery = "origin"

    webhook_url = webhook_url.strip()
    if delivery == "webhook" and not webhook_url:
        _emit_input_status("")
        return "webhook_url is required when delivery is 'webhook'."

    job_id = uuid4().hex[:12]
    now = datetime.utcnow().isoformat() + "Z"

    try:
        from chat_storage import _get_connection
        from persistence import get_current_agent_thread_id
        thread_id = get_current_agent_thread_id() or "default"

        conn = _get_connection()
        try:
            cron_expr = schedule.strip() if re.match(
                r"^[\d\*/,\-]+\s+[\d\*/,\-]+\s+[\d\*/,\-]+\s+[\d\*/,\-]+\s+[\d\*/,\-A-Za-z]+$",
                schedule.strip(),
            ) else ""
            conn.execute(
                """INSERT INTO scheduled_jobs
                   (id, prompt, schedule, delivery, origin_thread_id, repeat_count,
                    runs_completed, next_run_at, created_at, status,
                    timezone, cron_expr, model_override, webhook_url)
                   VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, 'active', ?, ?, ?, ?)""",
                (job_id, prompt, schedule, delivery, thread_id, repeat_count,
                 next_run.isoformat(), now, timezone, cron_expr, model_override,
                 webhook_url),
            )
            conn.commit()
        finally:
            conn.close()
    except Exception as e:
        _emit_input_status("")
        return f"Failed to save scheduled task: {e}"

    run_desc = f"(runs {repeat_count} time{'s' if repeat_count > 1 else ''})"
    _emit_input_status("")
    return (
        f"Scheduled task '{job_id}': {prompt[:60]}...\n"
        f"Next run: {next_run.strftime('%Y-%m-%d %H:%M')} {run_desc}\n"
        f"Delivery: {delivery}"
    )


@tool
def list_scheduled_tasks() -> str:
    """List scheduled tasks that are still active (waiting for their next run).

    Completed and cancelled jobs are omitted so the list reflects what will run next,
    not full history."""
    _debug_log("[TOOL] list_scheduled_tasks called", "TOOL")
    _emit_input_status("Listing scheduled tasks…")

    try:
        from chat_storage import _get_connection
        conn = _get_connection()
        try:
            rows = conn.execute(
                "SELECT id, prompt, schedule, status, next_run_at, runs_completed, repeat_count "
                "FROM scheduled_jobs WHERE status = 'active' "
                "ORDER BY (next_run_at IS NULL OR next_run_at = ''), next_run_at ASC, created_at ASC"
            ).fetchall()
        finally:
            conn.close()
    except Exception as e:
        _emit_input_status("")
        return f"Failed to list tasks: {e}"

    if not rows:
        _emit_input_status("")
        return "No active scheduled tasks."

    lines = [f"Active scheduled tasks ({len(rows)}):"]
    for job_id, prompt, schedule, status, next_run, completed, total in rows:
        short_prompt = prompt[:50] + ("..." if len(prompt) > 50 else "")
        progress = f"{completed}/{total}" if total > 1 else ""
        lines.append(
            f"- [{status}] {job_id}: {short_prompt}"
            + (f" | next: {next_run[:16]}" if next_run else "")
            + (f" | runs: {progress}" if progress else "")
        )

    _emit_input_status("")
    return "\n".join(lines)


class CancelTaskInput(BaseModel):
    job_id: str = Field(description="The ID of the scheduled task to cancel.")


@tool(args_schema=CancelTaskInput)
def cancel_scheduled_task(job_id: str) -> str:
    """Cancel a scheduled task by its ID."""
    _debug_log(f"[TOOL] cancel_scheduled_task called: {job_id}", "TOOL")
    _emit_input_status("Cancelling task…")

    try:
        from chat_storage import _get_connection
        conn = _get_connection()
        try:
            cursor = conn.execute(
                "UPDATE scheduled_jobs SET status = 'cancelled' WHERE id = ? AND status = 'active'",
                (job_id.strip(),),
            )
            conn.commit()
            if cursor.rowcount == 0:
                _emit_input_status("")
                return f"No active task found with ID '{job_id}'."
        finally:
            conn.close()
    except Exception as e:
        _emit_input_status("")
        return f"Failed to cancel task: {e}"

    _emit_input_status("")
    return f"Cancelled task '{job_id}'."
