"""
wivexx_goals.py — goals / to-do tracking with reminders  (v1.9)
═══════════════════════════════════════════════════════════════════════
A productivity-focused goal list built on what actually works in to-do
methodology:

  • PRIORITIES (high / normal / low) — high-priority goals float to the top
    and are what the overlay + "today focus" show first ("eat the frog").
  • DUE DATES — overdue goals are flagged loudly; "due today" is the focus.
  • TODAY FOCUS — the top 3 actionable goals (overdue → due today → high
    priority), because 3 visible priorities beat a 30-item wall (Ivy Lee).
  • REMINDERS per goal — hourly, daily at a time, or weekly on a day+time,
    delivered through WIVEXX's non-interrupting announcement queue.
  • STREAK & STATS — completions this week and a current daily-completion
    streak, for a small motivation loop.

Storage: wivexx_goals.json next to this file.

Public API:
    list_goals(include_done=False) -> list[dict]
    add_goal(title, priority="normal", due=None, reminder="none",
             remind_time="09:00", remind_day=0, notes="") -> dict
    update_goal(goal_id, **fields) -> bool
    complete_goal(goal_id) / reopen_goal(goal_id) / delete_goal(goal_id)
    today_focus() -> list[dict]           # top 3
    spoken_summary() -> str               # for the voice query
    stats() -> dict                       # {open, done_week, streak}
    start_reminders(announce, logger)     # background thread
"""

import datetime as _dt
import json
import os
import threading
import time
from typing import Callable, Optional

_GOALS_FILE = os.path.join(
    os.path.dirname(os.path.abspath(__file__)), "wivexx_goals.json"
)
_lock = threading.Lock()
_logger = None

PRIORITIES = ("high", "normal", "low")
REMINDERS  = ("none", "hourly", "daily", "weekly")
WEEKDAYS   = ("Monday", "Tuesday", "Wednesday", "Thursday",
              "Friday", "Saturday", "Sunday")


# ── Storage ────────────────────────────────────────────────────────────────────
def _load() -> dict:
    try:
        with open(_GOALS_FILE, encoding="utf-8") as f:
            data = json.load(f)
        if isinstance(data, dict) and isinstance(data.get("goals"), list):
            return data
    except Exception:
        pass
    return {"goals": [], "next_id": 1}


def _save(data: dict) -> None:
    try:
        with open(_GOALS_FILE, "w", encoding="utf-8") as f:
            json.dump(data, f, indent=2)
    except Exception as e:
        if _logger:
            _logger.warning("goals: save failed: %s", e)


# ── Core API ───────────────────────────────────────────────────────────────────
def list_goals(include_done: bool = False) -> list:
    """Open goals sorted by PRIORITY, then by time remaining — soonest `due`
    first (overdue at the top of the tier); goals with NO due date sit at the
    bottom of their priority tier."""
    with _lock:
        goals = _load()["goals"]
    if not include_done:
        goals = [g for g in goals if not g.get("done")]

    def _key(g):
        prio    = PRIORITIES.index(g.get("priority", "normal")) \
            if g.get("priority", "normal") in PRIORITIES else 1
        due     = g.get("due")
        has_due = 0 if due else 1           # within a tier: dated goals before undated
        return (g.get("done", False), prio, has_due, due or "9999-12-31",
                g.get("created", ""))
    return sorted((dict(g) for g in goals), key=_key)


def add_goal(title: str, priority: str = "normal", due: Optional[str] = None,
             reminder: str = "none", remind_time: str = "09:00",
             remind_day: int = 0, notes: str = "") -> dict:
    title = (title or "").strip()
    if not title:
        raise ValueError("empty goal title")
    with _lock:
        data = _load()
        goal = {
            "id": data.get("next_id", 1),
            "title": title[:200],
            "notes": (notes or "").strip()[:500],
            "priority": priority if priority in PRIORITIES else "normal",
            "due": due or None,                       # "YYYY-MM-DD" | None
            "reminder": reminder if reminder in REMINDERS else "none",
            "remind_time": remind_time or "09:00",    # "HH:MM"
            "remind_day": int(remind_day) % 7,        # 0=Monday (weekly)
            "created": _dt.datetime.now().strftime("%Y-%m-%d %H:%M"),
            "done": False,
            "done_at": None,
            "last_reminded": 0,
        }
        data["goals"].append(goal)
        data["next_id"] = goal["id"] + 1
        _save(data)
    if _logger:
        _logger.info("goals: added #%d %r", goal["id"], title)
    return dict(goal)


def update_goal(goal_id: int, **fields) -> bool:
    allowed = {"title", "notes", "priority", "due", "reminder",
               "remind_time", "remind_day", "done"}
    with _lock:
        data = _load()
        for g in data["goals"]:
            if g["id"] == int(goal_id):
                for k, v in fields.items():
                    if k in allowed:
                        g[k] = v
                if fields.get("done"):
                    g["done_at"] = _dt.datetime.now().strftime("%Y-%m-%d %H:%M")
                elif "done" in fields:
                    g["done_at"] = None
                _save(data)
                return True
    return False


def complete_goal(goal_id: int) -> bool:
    return update_goal(goal_id, done=True)


def reopen_goal(goal_id: int) -> bool:
    return update_goal(goal_id, done=False)


def delete_goal(goal_id: int) -> bool:
    with _lock:
        data = _load()
        before = len(data["goals"])
        data["goals"] = [g for g in data["goals"] if g["id"] != int(goal_id)]
        if len(data["goals"]) != before:
            _save(data)
            return True
    return False


# ── Productivity views ─────────────────────────────────────────────────────────
def today_focus() -> list:
    """The 3 goals that matter most right now."""
    return list_goals()[:3]


def stats() -> dict:
    with _lock:
        goals = _load()["goals"]
    open_n = sum(1 for g in goals if not g.get("done"))
    week_start = (_dt.date.today()
                  - _dt.timedelta(days=_dt.date.today().weekday())).isoformat()
    done_week = sum(1 for g in goals
                    if g.get("done") and (g.get("done_at") or "") >= week_start)
    # streak: consecutive days (ending today/yesterday) with ≥1 completion
    done_days = {(g.get("done_at") or "")[:10] for g in goals if g.get("done")}
    streak, day = 0, _dt.date.today()
    if day.isoformat() not in done_days:
        day -= _dt.timedelta(days=1)          # today not over yet — be kind
    while day.isoformat() in done_days:
        streak += 1
        day -= _dt.timedelta(days=1)
    return {"open": open_n, "done_week": done_week, "streak": streak}


def spoken_summary() -> str:
    """What WIVEXX says for 'what are my goals?'."""
    goals = list_goals()
    s = stats()
    if not goals:
        if s["done_week"]:
            return (f"Your goal list is clear, sir — and you've completed "
                    f"{s['done_week']} this week. Well done.")
        return "Your goal list is empty, sir. Say 'add goal' or use the Goals menu."
    today = _dt.date.today().isoformat()
    parts = []
    for g in goals[:5]:
        line = g["title"]
        if g.get("due"):
            if g["due"] < today:
                line += " — overdue"
            elif g["due"] == today:
                line += " — due today"
        if g.get("priority") == "high":
            line += " (high priority)"
        parts.append(line)
    more = f" …and {len(goals) - 5} more." if len(goals) > 5 else ""
    streak_txt = (f" Your completion streak is {s['streak']} "
                  f"day{'s' if s['streak'] != 1 else ''}." if s["streak"] else "")
    return (f"You have {len(goals)} open goal{'s' if len(goals) != 1 else ''}, sir: "
            + "; ".join(parts) + "." + more + streak_txt)


# ── Reminders ──────────────────────────────────────────────────────────────────
def _due_for_reminder(g: dict, now: _dt.datetime) -> bool:
    kind = g.get("reminder", "none")
    if kind == "none" or g.get("done"):
        return False
    last = float(g.get("last_reminded", 0))
    if kind == "hourly":
        return time.time() - last >= 3600
    try:
        hh, mm = (g.get("remind_time") or "09:00").split(":")
        target = now.replace(hour=int(hh), minute=int(mm),
                             second=0, microsecond=0)
    except Exception:
        target = now.replace(hour=9, minute=0, second=0, microsecond=0)
    # weekly: only on the chosen weekday
    if kind == "weekly" and now.weekday() != int(g.get("remind_day", 0)) % 7:
        return False
    # CATCH-UP: fire once any time AT or AFTER the target today — not just inside
    # a 2-minute window — so a reminder still lands if WIVEXX wasn't running at
    # the exact minute (PC asleep, started late…). Guard: not already fired today.
    if now < target:
        return False
    last_dt = _dt.datetime.fromtimestamp(last) if last else None
    if last_dt and last_dt.date() == now.date():
        return False
    return True


def start_reminders(announce: Callable[[str], None], logger=None,
                    alarm: Optional[Callable[[str], None]] = None) -> None:
    """Background thread: delivers per-goal reminders through *announce* (WIVEXX's
    non-interrupting spoken queue) and, if given, an *alarm* callback that pops a
    visible alarm at the reminder time (title)."""
    global _logger
    _logger = logger

    def _loop() -> None:
        while True:
            time.sleep(45)
            try:
                now = _dt.datetime.now()
                with _lock:
                    data = _load()
                    fired = []
                    for g in data["goals"]:
                        if _due_for_reminder(g, now):
                            g["last_reminded"] = time.time()
                            fired.append(dict(g))
                    if fired:
                        _save(data)
                for g in fired:
                    extra = ""
                    if g.get("due"):
                        if g["due"] < _dt.date.today().isoformat():
                            extra = " It is overdue."
                        elif g["due"] == _dt.date.today().isoformat():
                            extra = " It is due today."
                    announce(f"Goal reminder, sir — {g['title']}.{extra}")
                    if alarm:
                        try:
                            alarm(g["title"])      # visible alarm popup at the time
                        except Exception:
                            pass
                    if logger:
                        logger.info("goals: reminder fired for #%d", g["id"])
            except Exception as e:
                if logger:
                    logger.debug("goals reminder loop: %s", e)

    threading.Thread(target=_loop, daemon=True,
                     name="wivexx-goal-reminders").start()
