"""Cron schedule management for labwatch."""

import re
import shutil
import subprocess
import sys
from typing import List, Optional

# Marker suffix used to identify labwatch-managed cron entries
MARKER_PREFIX = "# labwatch:"

# Sentinel comments that wrap all labwatch entries in the crontab
_SENTINEL_BEGIN = "# ── LABWATCH ENTRIES (generated by labwatch init) ──"
_SENTINEL_END = "# ── END LABWATCH ENTRIES ──"

_INTERVAL_RE = re.compile(r"^(\d+)([mhdw])$")


def parse_interval(interval: str) -> str:
    """Convert a human interval like '5m', '4h', '1d', '1w' to a cron expression.

    Supported formats:
      5m  → */5 * * * *
      4h  → 0 */4 * * *
      1d  → 0 0 * * *
      1w  → 0 0 * * 0   (weekly, Sunday at midnight)
    """
    match = _INTERVAL_RE.match(interval.strip())
    if not match:
        raise ValueError(
            f"Invalid interval '{interval}'. Use format like 5m, 4h, 1d, 1w."
        )

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

    if unit == "m":
        if value < 1 or value > 59:
            raise ValueError(f"Minute interval must be 1-59, got {value}")
        return f"*/{value} * * * *"
    elif unit == "h":
        if value < 1 or value > 23:
            raise ValueError(f"Hour interval must be 1-23, got {value}")
        return f"0 */{value} * * *"
    elif unit == "d":
        if value != 1:
            raise ValueError("Day interval only supports 1d (daily at midnight)")
        return "0 0 * * *"
    elif unit == "w":
        if value != 1:
            raise ValueError("Week interval only supports 1w (weekly on Sunday)")
        return "0 0 * * 0"

    raise ValueError(f"Unknown unit '{unit}'")


def resolve_labwatch_path() -> str:
    """Find the absolute path to the labwatch executable.

    Tries shutil.which first, then falls back to 'python -m labwatch'.
    """
    path = shutil.which("labwatch")
    if path:
        return path
    # Fall back to running as a module with the current interpreter
    return f"{sys.executable} -m labwatch"


def _read_crontab() -> str:
    """Read the current user's crontab."""
    try:
        proc = subprocess.run(
            ["crontab", "-l"],
            capture_output=True, text=True, timeout=10,
        )
        # crontab -l returns 1 with "no crontab for user" on some systems
        if proc.returncode != 0 and "no crontab" in proc.stderr.lower():
            return ""
        return proc.stdout
    except FileNotFoundError:
        raise RuntimeError("crontab command not found. Is cron installed?")
    except subprocess.TimeoutExpired:
        raise RuntimeError("crontab -l timed out")


def _write_crontab(content: str) -> None:
    """Write content as the user's crontab."""
    try:
        proc = subprocess.run(
            ["crontab", "-"],
            input=content, capture_output=True, text=True, timeout=10,
        )
        if proc.returncode != 0:
            raise RuntimeError(f"crontab write failed: {proc.stderr.strip()}")
    except FileNotFoundError:
        raise RuntimeError("crontab command not found. Is cron installed?")
    except subprocess.TimeoutExpired:
        raise RuntimeError("crontab write timed out")


def _split_crontab(raw: str):
    """Split a raw crontab into (user_lines, labwatch_lines).

    Strips sentinel comments from the labwatch block so callers only see
    the actual cron entry lines.  User lines are everything outside the
    sentinel block (or the entire crontab if no block exists).  For
    backwards-compat, any ``# labwatch:`` lines found *outside* a sentinel
    block are migrated into the labwatch bucket automatically.
    """
    lines = raw.splitlines()
    user_lines: List[str] = []
    lw_lines: List[str] = []

    in_block = False
    for line in lines:
        if line.strip() == _SENTINEL_BEGIN:
            in_block = True
            continue
        if line.strip() == _SENTINEL_END:
            in_block = False
            continue
        if in_block:
            lw_lines.append(line)
        elif MARKER_PREFIX in line:
            # Legacy entry outside sentinel block — adopt it
            lw_lines.append(line)
        else:
            user_lines.append(line)

    return user_lines, lw_lines


def _join_crontab(user_lines: List[str], lw_lines: List[str]) -> str:
    """Reassemble a crontab from user lines and labwatch lines.

    Labwatch lines are wrapped in sentinel comments.  If there are no
    labwatch lines the sentinel block is omitted entirely.
    """
    parts = list(user_lines)

    if lw_lines:
        # Ensure a blank line before the block if user content exists
        if parts and parts[-1].strip():
            parts.append("")
        parts.append(_SENTINEL_BEGIN)
        parts.extend(lw_lines)
        parts.append(_SENTINEL_END)

    result = "\n".join(parts).strip()
    return (result + "\n") if result else ""


def list_entries() -> List[str]:
    """Return all labwatch-managed cron entries."""
    _check_platform()
    crontab = _read_crontab()
    _user, lw_lines = _split_crontab(crontab)
    return [line for line in lw_lines if MARKER_PREFIX in line]


def add_entry(subcommand: str, interval: str, modules: Optional[List[str]] = None) -> str:
    """Add (or replace) a labwatch cron entry for the given subcommand.

    If *modules* is provided (e.g. ["network", "dns"]), the entry is scoped
    to those check modules via ``--only`` and gets its own cron marker so it
    can coexist with other per-module entries.

    All labwatch entries are kept inside a sentinel block in the crontab.

    Returns the cron line that was added.
    """
    _check_platform()
    cron_expr = parse_interval(interval)
    labwatch_path = resolve_labwatch_path()

    if modules:
        modules_str = ",".join(sorted(modules))
        marker = f"{MARKER_PREFIX}{subcommand}:{modules_str}"
        cron_line = f"{cron_expr} {labwatch_path} {subcommand} --only {modules_str} {marker}"
    else:
        marker = f"{MARKER_PREFIX}{subcommand}"
        cron_line = f"{cron_expr} {labwatch_path} {subcommand} {marker}"

    crontab = _read_crontab()
    user_lines, lw_lines = _split_crontab(crontab)

    # Remove any existing entry with the exact same marker
    lw_lines = [line for line in lw_lines if marker not in line]
    lw_lines.append(cron_line)

    _write_crontab(_join_crontab(user_lines, lw_lines))
    return cron_line


def remove_entries(subcommand: Optional[str] = None) -> int:
    """Remove labwatch cron entries.

    If subcommand is given, only remove that entry.
    Otherwise, remove all labwatch entries.

    Returns the number of entries removed.
    """
    _check_platform()
    crontab = _read_crontab()
    user_lines, lw_lines = _split_crontab(crontab)

    if subcommand:
        marker = f"{MARKER_PREFIX}{subcommand}"
        new_lw = [line for line in lw_lines if marker not in line]
    else:
        new_lw = []

    removed = len(lw_lines) - len(new_lw)

    if removed > 0:
        _write_crontab(_join_crontab(user_lines, new_lw))

    return removed


def _check_platform() -> None:
    """Raise an error on unsupported platforms."""
    if sys.platform == "win32":
        raise RuntimeError(
            "Cron scheduling is not supported on Windows. "
            "Use Task Scheduler instead."
        )
