"""Cron expression parsing and schedule persistence for scheduled tasks."""

import json
import uuid
from datetime import datetime, timedelta
from pathlib import Path


def parse_cron(expr: str) -> dict:
    """Parse a 5-field cron expression into its components.

    Args:
        expr: A standard 5-field cron string
              (minute hour day_of_month month day_of_week).

    Returns:
        Dict with keys: minute, hour, dom, month, dow.

    Raises:
        ValueError: If the expression does not contain exactly 5 fields.
    """
    fields = expr.strip().split()
    if len(fields) != 5:
        raise ValueError(
            f"Expected 5 fields in cron expression, got {len(fields)}: {expr!r}"
        )
    return {
        "minute": fields[0],
        "hour": fields[1],
        "dom": fields[2],
        "month": fields[3],
        "dow": fields[4],
    }


def _field_matches(field_expr: str, value: int) -> bool:
    """Return True if a cron field expression matches the given integer value.

    Supported syntax:
        '*'     — matches any value
        '5'     — exact match
        '*/N'   — matches when value is divisible by N
        '1,5,9' — matches any value in the comma-separated list

    Each element in a comma-separated list may itself be '*', '*/N', or a number.
    """
    for part in field_expr.split(","):
        part = part.strip()
        if part == "*":
            return True
        if part.startswith("*/"):
            step = int(part[2:])
            if value % step == 0:
                return True
        else:
            if int(part) == value:
                return True
    return False


def cron_matches_now(cron_dict: dict, dt: datetime) -> bool:
    """Return True if the datetime matches all 5 fields of the cron dict.

    Args:
        cron_dict: Dict with keys minute, hour, dom, month, dow
                   (as returned by parse_cron).
        dt: The datetime to check against.

    Returns:
        True if all 5 cron fields match the datetime.
    """
    return (
        _field_matches(cron_dict["minute"], dt.minute)
        and _field_matches(cron_dict["hour"], dt.hour)
        and _field_matches(cron_dict["dom"], dt.day)
        and _field_matches(cron_dict["month"], dt.month)
        and _field_matches(cron_dict["dow"], dt.weekday())
    )


def next_run(cron_dict: dict, after: datetime) -> datetime:
    """Return the first datetime after *after* that matches the cron expression.

    Iterates minute-by-minute up to 366 days ahead.

    Args:
        cron_dict: Dict with keys minute, hour, dom, month, dow
                   (as returned by parse_cron).
        after: The starting datetime to search from (exclusive).

    Returns:
        The first matching datetime.

    Raises:
        ValueError: If no matching datetime is found within 366 days.
    """
    candidate = after.replace(second=0, microsecond=0) + timedelta(minutes=1)
    limit = after + timedelta(days=366)
    while candidate <= limit:
        if cron_matches_now(cron_dict, candidate):
            return candidate
        candidate += timedelta(minutes=1)
    raise ValueError(
        "No matching datetime found within 366 days of "
        f"{after.isoformat()}"
    )


class ScheduleStore:
    """Persistent store for scheduled chain executions.

    Schedules are saved to ~/.brynq/schedules.json.
    """

    def __init__(self) -> None:
        self._dir = Path.home() / ".brynq"
        self._path = self._dir / "schedules.json"
        self._schedules: dict[str, dict] = {}
        self._load()

    def _load(self) -> None:
        """Load schedules from disk, creating the file if missing."""
        if self._path.exists():
            data = json.loads(self._path.read_text(encoding="utf-8"))
            self._schedules = {s["id"]: s for s in data}
        else:
            self._dir.mkdir(parents=True, exist_ok=True)
            self._save()

    def _save(self) -> None:
        """Write current schedules to disk."""
        self._dir.mkdir(parents=True, exist_ok=True)
        self._path.write_text(
            json.dumps(list(self._schedules.values()), indent=2),
            encoding="utf-8",
        )

    def create(
        self,
        name: str,
        cron_expr: str,
        chain_template: str,
        enabled: bool = True,
    ) -> str:
        """Create a new schedule and persist it.

        Args:
            name: Human-readable schedule name.
            cron_expr: 5-field cron expression.
            chain_template: The chain template to execute.
            enabled: Whether the schedule is active.

        Returns:
            The generated schedule ID (uuid4).

        Raises:
            ValueError: If cron_expr is not a valid 5-field expression.
        """
        # Validate cron — parse_cron raises ValueError on bad input
        parse_cron(cron_expr)

        schedule_id = str(uuid.uuid4())
        self._schedules[schedule_id] = {
            "id": schedule_id,
            "name": name,
            "cron_expr": cron_expr,
            "chain_template": chain_template,
            "enabled": enabled,
            "last_run": None,
            "created_at": datetime.now().isoformat(),
        }
        self._save()
        return schedule_id

    def list_all(self) -> list[dict]:
        """Return all schedules with computed next_run times.

        Returns:
            List of dicts, each with keys: id, name, cron_expr,
            chain_template, enabled, last_run, next_run.
            next_run is an ISO-formatted datetime string for enabled
            schedules, or None for disabled ones / unparseable cron.
        """
        now = datetime.now()
        result = []
        for schedule in self._schedules.values():
            computed_next: str | None = None
            if schedule.get("enabled"):
                try:
                    cron_dict = parse_cron(schedule["cron_expr"])
                    computed_next = next_run(cron_dict, now).isoformat()
                except (ValueError, KeyError):
                    computed_next = None
            result.append({
                "id": schedule["id"],
                "name": schedule["name"],
                "cron_expr": schedule["cron_expr"],
                "chain_template": schedule["chain_template"],
                "enabled": schedule["enabled"],
                "last_run": schedule.get("last_run"),
                "next_run": computed_next,
            })
        return result

    def delete(self, schedule_id: str) -> bool:
        """Remove a schedule by ID.

        Args:
            schedule_id: The ID of the schedule to remove.

        Returns:
            True if the schedule was found and removed, False otherwise.
        """
        if schedule_id not in self._schedules:
            return False
        del self._schedules[schedule_id]
        self._save()
        return True

    def get_due(self) -> list[dict]:
        """Return all enabled schedules whose cron expression matches now.

        Returns:
            List of schedule dicts whose cron_expr matches the current
            minute (via cron_matches_now). Schedules with invalid cron
            expressions are silently skipped.
        """
        now = datetime.now().replace(second=0, microsecond=0)
        due = []
        for schedule in self._schedules.values():
            if not schedule.get("enabled"):
                continue
            try:
                cron_dict = parse_cron(schedule["cron_expr"])
            except (ValueError, KeyError):
                continue
            if cron_matches_now(cron_dict, now):
                due.append(dict(schedule))
        return due

    def record_run(self, schedule_id: str, status: str, output_summary: str) -> bool:
        """Append a run record to history and update last_run on the schedule.

        Args:
            schedule_id: The ID of the schedule that was executed.
            status: Outcome of the run (e.g. "success", "error").
            output_summary: Brief description of what happened.

        Returns:
            True if the schedule was found and updated, False otherwise.
        """
        if schedule_id not in self._schedules:
            return False

        now = datetime.now().isoformat()

        # Append to history file
        history_path = self._dir / "schedule_history.jsonl"
        record = {
            "schedule_id": schedule_id,
            "status": status,
            "output_summary": output_summary,
            "timestamp": now,
        }
        self._dir.mkdir(parents=True, exist_ok=True)
        with open(history_path, "a", encoding="utf-8") as f:
            f.write(json.dumps(record) + "\n")

        # Update last_run on the schedule
        self._schedules[schedule_id]["last_run"] = now
        self._save()
        return True

    def get_history(self, schedule_id: str, limit: int = 20) -> list[dict]:
        """Read run history for a specific schedule.

        Args:
            schedule_id: The schedule to retrieve history for.
            limit: Maximum number of entries to return (most recent first).

        Returns:
            List of history dicts (newest first), each with keys:
            schedule_id, status, output_summary, timestamp.
        """
        history_path = self._dir / "schedule_history.jsonl"
        if not history_path.exists():
            return []

        entries = []
        for line in history_path.read_text(encoding="utf-8").splitlines():
            line = line.strip()
            if not line:
                continue
            try:
                record = json.loads(line)
            except json.JSONDecodeError:
                continue
            if record.get("schedule_id") == schedule_id:
                entries.append(record)

        return entries[-limit:]

    def enable(self, schedule_id: str, enabled: bool) -> bool:
        """Set the enabled flag on a schedule.

        Args:
            schedule_id: The ID of the schedule to update.
            enabled: Whether the schedule should be active.

        Returns:
            True if the schedule was found and updated, False otherwise.
        """
        if schedule_id not in self._schedules:
            return False
        self._schedules[schedule_id]["enabled"] = enabled
        self._save()
        return True
