"""
Dashboard data manager — reads and writes dashboard-data.json
with file locking and atomic writes to prevent corruption.
"""

import json
import os
import tempfile
import logging
from datetime import datetime, date
from pathlib import Path
from filelock import FileLock, Timeout

logger = logging.getLogger(__name__)


class DashboardManager:
    def __init__(self, file_path: str):
        self.file_path = Path(file_path)
        self.lock_path = str(self.file_path) + ".lock"
        self.lock = FileLock(self.lock_path, timeout=10)

    # ── Read ──────────────────────────────────────────────────

    def read(self) -> dict:
        """Read and return the full dashboard data."""
        with self.lock:
            with open(self.file_path, "r", encoding="utf-8") as f:
                return json.load(f)

    # ── Atomic Write ──────────────────────────────────────────

    def _write(self, data: dict):
        """Atomic write: write to temp file, then rename."""
        dir_path = self.file_path.parent
        fd, tmp_path = tempfile.mkstemp(dir=str(dir_path), suffix=".json.tmp")
        try:
            with os.fdopen(fd, "w", encoding="utf-8") as f:
                json.dump(data, f, indent=2, ensure_ascii=False)
            os.replace(tmp_path, str(self.file_path))
            logger.info("Dashboard data written successfully")
        except Exception:
            if os.path.exists(tmp_path):
                os.unlink(tmp_path)
            raise

    def _read_and_write(self, updater):
        """Read data, apply updater function, write back. Returns updated data."""
        with self.lock:
            with open(self.file_path, "r", encoding="utf-8") as f:
                data = json.load(f)
            result = updater(data)
            self._write(data)
            return data, result

    # ── Weight ────────────────────────────────────────────────

    def update_weight(self, value: float) -> str:
        """Update weight with new value, rotate series, recalculate derived fields."""
        def updater(data):
            w = data["weight"]
            old = w["current"]
            w["deltaVsLast"] = round(value - old, 1)
            w["current"] = value

            # Rotate series: drop oldest, append new
            series = w.get("series30d", [])
            series.append(value)
            if len(series) > 30:
                series = series[-30:]
            w["series30d"] = series

            # Recalculate 7-day average
            last7 = series[-7:] if len(series) >= 7 else series
            w["avg7d"] = round(sum(last7) / len(last7), 1)

            # Recalculate pace and ETA
            baseline = w.get("baseline", 245.0)
            goal = w.get("goal", 220)
            baseline_date = datetime(2026, 2, 1)
            weeks_elapsed = max((datetime.now() - baseline_date).days / 7, 1)
            total_lost = baseline - value
            pace = round(total_lost / weeks_elapsed, 1)
            w["pace"] = pace
            remaining = value - goal
            w["etaWeeks"] = round(remaining / pace) if pace > 0 else 999

            today = datetime.now()
            w["lastWeighIn"] = today.strftime("%B %-d")
            data["date"] = today.strftime("%Y-%m-%d")

            return f"Weight updated to {value} lb (was {old}). Change: {w['deltaVsLast']:+.1f} lb. Pace: {pace} lb/wk. ETA to {goal}: ~{w['etaWeeks']} weeks."

        _, msg = self._read_and_write(updater)
        return msg

    # ── Body Fat ──────────────────────────────────────────────

    def update_body_fat(self, value: float) -> str:
        """Update body fat percentage, rotate series."""
        def updater(data):
            bf = data["bodyFat"]
            old = bf["current"]
            bf["deltaVsLast"] = round(value - old, 1)
            bf["current"] = value

            series = bf.get("series30d", [])
            series.append(value)
            if len(series) > 30:
                series = series[-30:]
            bf["series30d"] = series

            return f"Body fat updated to {value}% (was {old}%). Change: {bf['deltaVsLast']:+.1f}%."

        _, msg = self._read_and_write(updater)
        return msg

    # ── Blood Pressure ────────────────────────────────────────

    def update_blood_pressure(self, systolic: int, diastolic: int) -> str:
        """Update blood pressure reading."""
        def updater(data):
            bp = data["bloodPressure"]
            old_s, old_d = bp["systolic"], bp["diastolic"]
            bp["systolic"] = systolic
            bp["diastolic"] = diastolic
            bp["date"] = date.today().isoformat()

            # Classify
            if systolic < 120 and diastolic < 80:
                bp["status"] = "normal"
            elif systolic < 130 and diastolic < 80:
                bp["status"] = "elevated"
            elif systolic < 140 or diastolic < 90:
                bp["status"] = "high-stage1"
            else:
                bp["status"] = "high-stage2"

            return f"BP updated to {systolic}/{diastolic} ({bp['status']}). Was {old_s}/{old_d}."

        _, msg = self._read_and_write(updater)
        return msg

    # ── Schedule ──────────────────────────────────────────────

    def add_event(self, event_date: str, time: str, title: str,
                  location: str = "", note: str = "",
                  end_date: str = None) -> str:
        """Add event to schedule. event_date should be YYYY-MM-DD.
        If end_date is provided, creates a multi-day event spanning the range."""
        from datetime import date as dt_date, timedelta

        def updater(data):
            if end_date and end_date != event_date:
                # Multi-day event: create an entry for each day
                start = dt_date.fromisoformat(event_date)
                end = dt_date.fromisoformat(end_date)
                days_added = 0
                current = start
                while current <= end:
                    event = {
                        "date": current.isoformat(),
                        "time": time,
                        "title": title,
                        "location": location,
                        "note": note,
                    }
                    data["schedule"].append(event)
                    days_added += 1
                    current += timedelta(days=1)
                data["schedule"].sort(key=lambda e: e.get("date", ""))
                return f"Added: {title} from {event_date} to {end_date} ({days_added} days). Location: {location or 'TBD'}."
            else:
                event = {
                    "date": event_date,
                    "time": time,
                    "title": title,
                    "location": location,
                    "note": note,
                }
                data["schedule"].append(event)
                data["schedule"].sort(key=lambda e: e.get("date", ""))
                return f"Added: {title} on {event_date} at {time}. Location: {location or 'TBD'}."

        _, msg = self._read_and_write(updater)
        return msg

    def remove_event(self, title_fragment: str, event_date: str = None) -> str:
        """Remove event(s) matching title (case-insensitive). Optionally filter by date."""
        def updater(data):
            before = len(data["schedule"])
            title_lower = title_fragment.lower()
            data["schedule"] = [
                e for e in data["schedule"]
                if not (
                    title_lower in e["title"].lower()
                    and (event_date is None or e["date"] == event_date)
                )
            ]
            removed = before - len(data["schedule"])
            if removed:
                return f"Removed {removed} event(s) matching '{title_fragment}'."
            return f"No events found matching '{title_fragment}'."

        _, msg = self._read_and_write(updater)
        return msg

    # ── Signals ───────────────────────────────────────────────

    def add_signal(self, cat: str, title: str, why: str, source: str = "") -> str:
        """Add a curated signal to the top of the list."""
        def updater(data):
            signal = {"cat": cat, "title": title, "why": why, "source": source}
            data["signals"]["curated"].insert(0, signal)
            data["signals"]["updated"] = date.today().isoformat()
            # Keep max 20 curated signals
            data["signals"]["curated"] = data["signals"]["curated"][:20]
            return f"Signal added: [{cat}] {title}"

        _, msg = self._read_and_write(updater)
        return msg

    # ── Focus ─────────────────────────────────────────────────

    def update_focus(self, text: str) -> str:
        """Update the focus/goal text."""
        def updater(data):
            old = data["focus"]
            data["focus"] = text
            return f"Focus updated to: '{text}' (was: '{old}')."

        _, msg = self._read_and_write(updater)
        return msg

    # ── Supplements ───────────────────────────────────────────

    def add_supplement(self, timing: str, name: str, dose: str,
                       brand: str = "", note: str = "") -> str:
        """Add a supplement. timing is 'morning', 'evening', or 'anytime'."""
        def updater(data):
            supp = {"name": name, "dose": dose, "brand": brand, "note": note}
            if timing not in data["supplements"]:
                data["supplements"][timing] = []
            data["supplements"][timing].append(supp)
            return f"Added {name} ({dose}) to {timing} supplements."

        _, msg = self._read_and_write(updater)
        return msg

    def remove_supplement(self, name_fragment: str) -> str:
        """Remove supplement matching name (case-insensitive) from any timing group."""
        def updater(data):
            name_lower = name_fragment.lower()
            removed = []
            for timing in ["morning", "evening", "anytime"]:
                before = len(data["supplements"].get(timing, []))
                data["supplements"][timing] = [
                    s for s in data["supplements"].get(timing, [])
                    if name_lower not in s["name"].lower()
                ]
                diff = before - len(data["supplements"][timing])
                if diff:
                    removed.append(f"{diff} from {timing}")
            if removed:
                return f"Removed supplement '{name_fragment}': {', '.join(removed)}."
            return f"No supplement found matching '{name_fragment}'."

        _, msg = self._read_and_write(updater)
        return msg

    # ── Viome Foods ───────────────────────────────────────────

    def update_viome(self, category: str, foods: list) -> str:
        """Replace a viome food category. category: superfoods/enjoy/minimize/avoid."""
        def updater(data):
            if category not in ["superfoods", "enjoy", "minimize", "avoid"]:
                return f"Invalid category '{category}'. Use: superfoods, enjoy, minimize, avoid."
            data["viome"][category] = foods
            return f"Viome '{category}' updated with {len(foods)} items."

        _, msg = self._read_and_write(updater)
        return msg

    def add_viome_food(self, category: str, food: str) -> str:
        """Add a single food to a viome category."""
        def updater(data):
            if category not in data["viome"]:
                return f"Invalid category '{category}'."
            if food not in data["viome"][category]:
                data["viome"][category].append(food)
                return f"Added '{food}' to viome {category}."
            return f"'{food}' is already in viome {category}."

        _, msg = self._read_and_write(updater)
        return msg

    # ── CRO Anchor ─────────────────────────────────────────────

    def update_cro_anchor(self, price: float = None) -> str:
        """Reset the CRO swing-trade anchor to the given price (or live price from CoinGecko)."""
        import requests as http_requests

        if price is None:
            # Fetch live CRO price from CoinGecko
            try:
                resp = http_requests.get(
                    "https://api.coingecko.com/api/v3/simple/price",
                    params={"ids": "crypto-com-chain", "vs_currencies": "usd"},
                    timeout=10,
                )
                resp.raise_for_status()
                price = resp.json()["crypto-com-chain"]["usd"]
            except Exception as e:
                logger.error(f"Failed to fetch CRO price: {e}")
                return f"Couldn't fetch live CRO price: {str(e)[:80]}. Try again or text 'cro anchor 0.085' with a manual price."

        def updater(data):
            strat = data.get("crypto", {}).get("position", {}).get("strategy", {})
            old_anchor = strat.get("anchorPrice", 0)
            strat["anchorPrice"] = round(price, 6)

            sell_pct = strat.get("sellTrigger", 0.07)
            buy_pct = strat.get("buyTrigger", 0.05)
            sell_target = round(price * (1 + sell_pct), 4)
            buy_target = round(price * (1 - buy_pct), 4)

            return (
                f"CRO anchor reset to ${price:.4f} (was ${old_anchor:.4f}).\n"
                f"New targets — Sell: ${sell_target} | Buy: ${buy_target}\n"
                f"Dashboard will update on next refresh."
            )

        _, msg = self._read_and_write(updater)
        return msg

    # ── Queries ───────────────────────────────────────────────

    def query_weight(self) -> str:
        data = self.read()
        w = data["weight"]
        return (
            f"Weight: {w['current']} lb (7-day avg: {w['avg7d']})\n"
            f"Goal: {w['goal']} lb | Baseline: {w['baseline']} lb ({w['baselineDate']})\n"
            f"Pace: {w['pace']} {w['paceUnit']} | ETA: ~{w['etaWeeks']} weeks\n"
            f"Last weigh-in: {w['lastWeighIn']}"
        )

    def query_body_fat(self) -> str:
        data = self.read()
        bf = data["bodyFat"]
        return (
            f"Body Fat: {bf['current']}% (goal: {bf['goal']}%)\n"
            f"Baseline: {bf['baseline']}% | Change vs last: {bf['deltaVsLast']:+.1f}%"
        )

    def query_blood_pressure(self) -> str:
        data = self.read()
        bp = data["bloodPressure"]
        return f"BP: {bp['systolic']}/{bp['diastolic']} ({bp['status']}) — measured {bp['date']}"

    def query_schedule(self, days_ahead: int = 14) -> str:
        data = self.read()
        today = date.today()
        upcoming = []
        for e in data["schedule"]:
            try:
                edate = date.fromisoformat(e["date"])
                if today <= edate <= today.replace(
                    day=min(today.day + days_ahead, 28)
                ):
                    upcoming.append(e)
            except (ValueError, KeyError):
                continue

        if not upcoming:
            return f"No events in the next {days_ahead} days."

        lines = [f"Upcoming events (next {days_ahead} days):"]
        for e in upcoming[:10]:
            time_str = f" {e['time']}" if e.get("time") else ""
            loc_str = f" @ {e['location']}" if e.get("location") else ""
            lines.append(f"• {e['date']}{time_str} — {e['title']}{loc_str}")
        return "\n".join(lines)

    def query_supplements(self) -> str:
        data = self.read()
        s = data["supplements"]
        lines = ["Supplements:"]
        for timing in ["morning", "evening", "anytime"]:
            items = s.get(timing, [])
            if items:
                lines.append(f"\n{timing.upper()}:")
                for item in items:
                    lines.append(f"  • {item['name']} — {item['dose']}")
        return "\n".join(lines)

    def query_signals(self) -> str:
        data = self.read()
        sigs = data["signals"]["curated"][:5]
        lines = [f"Latest signals (updated {data['signals']['updated']}):"]
        for s in sigs:
            lines.append(f"[{s['cat']}] {s['title']}")
        return "\n".join(lines)

    def query_summary(self) -> str:
        """Full dashboard summary."""
        data = self.read()
        w = data["weight"]
        bf = data["bodyFat"]
        bp = data["bloodPressure"]
        sched_count = len([
            e for e in data["schedule"]
            if e.get("date", "") >= date.today().isoformat()
        ])
        return (
            f"Dashboard Summary ({data['date']}):\n"
            f"Weight: {w['current']} lb (goal {w['goal']}) | Pace: {w['pace']} lb/wk\n"
            f"Body Fat: {bf['current']}% (goal {bf['goal']}%)\n"
            f"BP: {bp['systolic']}/{bp['diastolic']} ({bp['status']})\n"
            f"Focus: {data['focus']}\n"
            f"Upcoming events: {sched_count}\n"
            f"Signals: {len(data['signals']['curated'])} curated"
        )
