"""
Oura Ring API v2 client.

Pulls sleep, HRV, steps, exercise, heart rate, and readiness data
directly from the Oura API — no Health Connect zip needed.

Bill wears his Oura ring, data syncs to Oura cloud, this module
pulls it via API and transforms it into dashboard-data.json format.

Docs: https://cloud.ouraring.com/v2/docs
Auth: Personal Access Token (Bearer token)
"""

import os
import json
import logging
import requests
from datetime import date, datetime, timedelta
from pathlib import Path

logger = logging.getLogger("oura-api")

# Token file location
_BASE_DIR = Path(__file__).resolve().parent.parent  # ~/clawd/
TOKEN_PATH = _BASE_DIR / ".oura-token.json"

# Oura API base URL
BASE_URL = "https://api.ouraring.com"


class OuraClient:
    """Oura Ring API v2 client."""

    def __init__(self, access_token: str = None):
        self._token = access_token or self._load_token()
        self._available = None

    @property
    def available(self) -> bool:
        """Check if Oura API is configured and accessible."""
        if self._available is None:
            if not self._token:
                self._available = False
                logger.info("Oura API: NOT CONFIGURED (no token found)")
            else:
                # Quick validation — try personal info endpoint
                try:
                    self._api_get("/v2/usercollection/personal_info")
                    self._available = True
                    logger.info("Oura API: ACTIVE (token validated)")
                except Exception as e:
                    # Don't cache SSL failures — might be transient
                    if "CERTIFICATE_VERIFY_FAILED" in str(e) or "SSL" in str(e):
                        logger.warning(f"Oura API: SSL error (will retry next call) — {e}")
                        return False  # Don't cache, allow retry
                    self._available = False
                    logger.warning(f"Oura API: UNAVAILABLE — {e}")
        return self._available

    def _load_token(self) -> str:
        """Load access token from file or env."""
        # Try env var first
        token = os.getenv("OURA_ACCESS_TOKEN", "").strip()
        if token:
            logger.info(f"Oura token loaded from env var ({len(token)} chars)")
            return token

        # Try token file (primary path)
        if TOKEN_PATH.exists():
            try:
                data = json.loads(TOKEN_PATH.read_text())
                token = data.get("access_token", "").strip()
                if token:
                    logger.info(f"Oura token loaded from {TOKEN_PATH} ({len(token)} chars)")
                    return token
            except Exception as e:
                logger.warning(f"Failed to load Oura token from {TOKEN_PATH}: {e}")

        # Fallback: try home dir path directly (in case __file__ resolves oddly)
        home_path = Path.home() / "clawd" / ".oura-token.json"
        if home_path.exists() and home_path != TOKEN_PATH:
            try:
                data = json.loads(home_path.read_text())
                token = data.get("access_token", "")
                if token:
                    logger.info(f"Oura token loaded from fallback {home_path}")
                    return token
            except Exception as e:
                logger.warning(f"Failed to load Oura token from {home_path}: {e}")

        logger.info(f"No Oura token found (checked env, {TOKEN_PATH}, {home_path})")
        return ""

    def _api_get(self, endpoint: str, params: dict = None) -> dict:
        """Make a GET request to the Oura API."""
        url = BASE_URL + endpoint

        resp = requests.get(
            url,
            params=params,
            headers={
                "Authorization": f"Bearer {self._token}",
                "Accept": "application/json",
            },
            timeout=15,
        )

        if resp.status_code == 401:
            logger.error(f"Oura API 401: Unauthorized — token: {self._token[:4]}...{self._token[-4:]}")
            resp.raise_for_status()
        elif resp.status_code == 429:
            logger.warning("Oura API: Rate limited — try again later")
            resp.raise_for_status()
        elif resp.status_code != 200:
            logger.error(f"Oura API error {resp.status_code}: {resp.text[:200]}")
            resp.raise_for_status()

        return resp.json()

    # ── Sleep ─────────────────────────────────────────────────

    def get_sleep(self, start_date: date = None, end_date: date = None) -> dict:
        """
        Get sleep data. Returns the most recent night's sleep
        in dashboard format.
        """
        if not end_date:
            end_date = date.today()
        if not start_date:
            start_date = end_date - timedelta(days=1)

        data = self._api_get("/v2/usercollection/sleep", {
            "start_date": start_date.isoformat(),
            "end_date": end_date.isoformat(),
        })

        sessions = data.get("data", [])
        if not sessions:
            return None

        # Find the longest session (main sleep, not naps)
        main = max(sessions, key=lambda s: s.get("total_sleep_duration", 0))
        return self._transform_sleep(main)

    def get_sleep_series(self, days: int = 14) -> dict:
        """Get sleep durations for the last N days."""
        end = date.today()
        start = end - timedelta(days=days)

        data = self._api_get("/v2/usercollection/sleep", {
            "start_date": start.isoformat(),
            "end_date": end.isoformat(),
        })

        # Group by date, take longest session per night
        by_date = {}
        for s in data.get("data", []):
            day = s.get("day", "")
            hours = s.get("total_sleep_duration", 0) / 3600
            if day and (day not in by_date or hours > by_date[day]):
                by_date[day] = round(hours, 1)

        return dict(sorted(by_date.items()))

    def _transform_sleep(self, sleep: dict) -> dict:
        """Transform Oura sleep response → dashboard format."""
        bedtime_str = sleep.get("bedtime_start", "")
        waketime_str = sleep.get("bedtime_end", "")

        # Parse ISO timestamps to HH:MM
        bedtime = ""
        wake_time = ""
        try:
            if bedtime_str:
                bedtime = datetime.fromisoformat(bedtime_str.replace("Z", "+00:00")).strftime("%H:%M")
            if waketime_str:
                wake_time = datetime.fromisoformat(waketime_str.replace("Z", "+00:00")).strftime("%H:%M")
        except (ValueError, TypeError):
            pass

        total_hours = round(sleep.get("total_sleep_duration", 0) / 3600, 1)

        # Sleep stages (seconds → minutes)
        stages = {
            "deep_min": round(sleep.get("deep_sleep_duration", 0) / 60),
            "rem_min": round(sleep.get("rem_sleep_duration", 0) / 60),
            "light_min": round(sleep.get("light_sleep_duration", 0) / 60),
            "awake_min": round(sleep.get("awake_time", 0) / 60),
        }

        # HRV
        avg_hrv = sleep.get("average_hrv")
        if avg_hrv is None:
            # Try from HRV field
            hrv_data = sleep.get("hrv", {})
            if isinstance(hrv_data, dict):
                avg_hrv = hrv_data.get("mean_rmssd")

        return {
            "date": sleep.get("day", date.today().isoformat()),
            "bedtime": bedtime,
            "wake_time": wake_time,
            "total_hours": total_hours,
            "source": "Oura",
            "stages": stages,
            "avg_hrv": round(avg_hrv, 1) if avg_hrv else None,
        }

    # ── Daily Activity (Steps) ────────────────────────────────

    def get_daily_activity(self, days: int = 7) -> dict:
        """Get daily step counts for the last N days."""
        end = date.today()
        start = end - timedelta(days=days)

        data = self._api_get("/v2/usercollection/daily_activity", {
            "start_date": start.isoformat(),
            "end_date": end.isoformat(),
        })

        steps = {}
        for day_data in data.get("data", []):
            day = day_data.get("day", "")
            count = day_data.get("steps", 0)
            if day and count > 0:
                steps[day] = count

        return dict(sorted(steps.items()))

    # ── Exercise Sessions ─────────────────────────────────────

    def get_sessions(self, days: int = 7) -> list:
        """Get exercise/workout sessions from both sessions and workouts endpoints."""
        end = date.today()
        start = end - timedelta(days=days)

        exercises = []

        # Pull from /sessions (manually tagged: meditation, breathing, etc.)
        try:
            data = self._api_get("/v2/usercollection/sessions", {
                "start_date": start.isoformat(),
                "end_date": end.isoformat(),
            })
            for s in data.get("data", []):
                parsed = self._parse_exercise_entry(s, source="session")
                if parsed:
                    exercises.append(parsed)
            logger.info(f"Oura sessions endpoint: {len(data.get('data', []))} entries")
        except Exception as e:
            logger.warning(f"Oura sessions endpoint failed: {e}")

        # Pull from /workout (auto-detected + app-started workouts)
        try:
            data = self._api_get("/v2/usercollection/workout", {
                "start_date": start.isoformat(),
                "end_date": end.isoformat(),
            })
            for w in data.get("data", []):
                parsed = self._parse_exercise_entry(w, source="workout")
                if parsed:
                    exercises.append(parsed)
            logger.info(f"Oura workout endpoint: {len(data.get('data', []))} entries")
        except Exception as e:
            logger.warning(f"Oura workout endpoint failed: {e}")

        # Deduplicate by date+time (same session might appear in both)
        seen = set()
        unique = []
        for ex in exercises:
            key = f"{ex['date']}_{ex['time']}_{ex['duration_min']}"
            if key not in seen:
                seen.add(key)
                unique.append(ex)

        # Sort newest first
        unique.sort(key=lambda x: (x["date"], x["time"]), reverse=True)
        return unique[:10]

    def _parse_exercise_entry(self, entry: dict, source: str = "session") -> dict:
        """Parse a session or workout entry into dashboard format."""
        start_str = entry.get("start_datetime", "")
        try:
            start_dt = datetime.fromisoformat(start_str.replace("Z", "+00:00"))
        except (ValueError, TypeError):
            return None

        # Duration from start/end
        end_str = entry.get("end_datetime", "")
        duration_min = 0
        if end_str:
            try:
                end_dt = datetime.fromisoformat(end_str.replace("Z", "+00:00"))
                duration_min = round((end_dt - start_dt).total_seconds() / 60)
            except (ValueError, TypeError):
                pass

        # Map Oura activity type
        activity_type = entry.get("type", entry.get("activity", "other"))
        type_map = {
            "cycling": "Biking", "running": "Running", "walking": "Walking",
            "hiking": "Hiking", "swimming": "Swimming", "yoga": "Yoga",
            "strength_training": "Strength Training", "hiit": "HIIT",
            "elliptical": "Elliptical", "dancing": "Dancing",
            "pilates": "Pilates", "stretching": "Stretching",
            "meditation": "Meditation", "other": "Other",
            "indoor_cycling": "Spinning", "outdoor_cycling": "Biking",
            "indoor_running": "Treadmill", "outdoor_running": "Running",
        }
        etype = type_map.get(activity_type, activity_type.replace("_", " ").title())

        # Name: try label, mood, or fall back to type
        name = entry.get("label") or entry.get("mood") or etype

        return {
            "date": start_dt.strftime("%Y-%m-%d"),
            "time": start_dt.strftime("%H:%M"),
            "type": etype,
            "name": name,
            "duration_min": duration_min,
        }

    # ── Heart Rate ────────────────────────────────────────────

    def get_heart_rate(self, days: int = 1) -> dict:
        """Get heart rate data summary."""
        end = date.today()
        start = end - timedelta(days=days)

        data = self._api_get("/v2/usercollection/heartrate", {
            "start_date": start.isoformat(),
            "end_date": end.isoformat(),
        })

        readings = data.get("data", [])
        if not readings:
            return None

        bpms = [r["bpm"] for r in readings if "bpm" in r]
        if not bpms:
            return None

        return {
            "avg": round(sum(bpms) / len(bpms), 1),
            "min": min(bpms),
            "max": max(bpms),
            "readings": len(bpms),
        }

    # ── Readiness ─────────────────────────────────────────────

    def get_readiness(self, days: int = 1) -> dict:
        """Get daily readiness score."""
        end = date.today()
        start = end - timedelta(days=days)

        data = self._api_get("/v2/usercollection/daily_readiness", {
            "start_date": start.isoformat(),
            "end_date": end.isoformat(),
        })

        entries = data.get("data", [])
        if not entries:
            return None

        latest = entries[-1]
        return {
            "score": latest.get("score"),
            "date": latest.get("day"),
            "contributors": latest.get("contributors", {}),
        }

    # ── Full Health Fetch ─────────────────────────────────────

    def fetch_all_health_data(self) -> dict:
        """
        Pull all available health data from Oura.
        Returns dict in dashboard-ready format.
        Each metric is independent — if one fails, others still return.
        Also stores errors in result["_errors"] for debugging.
        """
        result = {}
        errors = []
        today = date.today()

        # Sleep (last night)
        try:
            sleep = self.get_sleep()
            if sleep:
                result["sleep"] = sleep
                series = self.get_sleep_series(14)
                if series:
                    result["sleep"]["series14d"] = series
                logger.info(f"Oura sleep: {sleep['total_hours']}h")
        except Exception as e:
            errors.append(f"sleep: {e}")
            logger.warning(f"Oura sleep fetch failed: {e}", exc_info=True)

        # Steps (7 days)
        try:
            steps = self.get_daily_activity(7)
            if steps:
                result["steps"] = {"daily": steps, "updated": today.isoformat()}
                logger.info(f"Oura steps: {len(steps)} days")
        except Exception as e:
            errors.append(f"steps: {e}")
            logger.warning(f"Oura steps fetch failed: {e}", exc_info=True)

        # Exercise sessions (7 days)
        try:
            exercises = self.get_sessions(7)
            if exercises:
                result["exercise"] = {"recent": exercises, "updated": today.isoformat()}
                logger.info(f"Oura exercise: {len(exercises)} sessions")
        except Exception as e:
            errors.append(f"exercise: {e}")
            logger.warning(f"Oura exercise fetch failed: {e}", exc_info=True)

        # Heart rate
        try:
            hr = self.get_heart_rate()
            if hr:
                result["heart_rate"] = hr
                logger.info(f"Oura HR: avg {hr['avg']} bpm")
        except Exception as e:
            errors.append(f"heart_rate: {e}")
            logger.warning(f"Oura heart rate fetch failed: {e}", exc_info=True)

        # Readiness
        try:
            readiness = self.get_readiness()
            if readiness:
                result["readiness"] = readiness
                logger.info(f"Oura readiness: {readiness.get('score')}")
        except Exception as e:
            errors.append(f"readiness: {e}")
            logger.warning(f"Oura readiness fetch failed: {e}", exc_info=True)

        if errors:
            result["_errors"] = errors
            logger.warning(f"Oura fetch completed with {len(errors)} errors: {errors}")

        return result


def ingest_oura_data(dashboard_manager) -> str:
    """
    Pull data from Oura API and update the dashboard.
    Returns a summary string.
    """
    client = OuraClient()

    # Check token exists (without making an API call)
    if not client._token:
        return "Oura API not configured. Run setup_oura_auth.py first."

    # Try fetching data directly — this is the real test
    try:
        data = client.fetch_all_health_data()
    except Exception as e:
        err = str(e)
        if "CERTIFICATE_VERIFY_FAILED" in err or "SSL" in err:
            return f"Oura API SSL error — try: pip3 install certifi\nDetail: {err[:120]}"
        return f"Oura API error: {err[:150]}"

    # Check for errors reported during fetch
    fetch_errors = data.pop("_errors", [])

    if not data:
        if fetch_errors:
            err_summary = "; ".join(str(e)[:80] for e in fetch_errors[:3])
            return f"Oura API calls failed:\n{err_summary}"
        return "No data returned from Oura API."

    changes = []

    def updater(dashboard):
        nonlocal changes

        # Sleep
        if "sleep" in data:
            dashboard["sleep"] = data["sleep"]
            changes.append(f"Sleep: {data['sleep']['total_hours']}h")

        # Steps
        if "steps" in data:
            dashboard["steps"] = data["steps"]
            total_today = data["steps"]["daily"].get(date.today().isoformat(), 0)
            changes.append(f"Steps: {total_today:,} today")

        # Exercise
        if "exercise" in data:
            dashboard["exercise"] = data["exercise"]
            changes.append(f"Exercise: {len(data['exercise']['recent'])} sessions")

        # Readiness (store if field exists or create it)
        if "readiness" in data:
            dashboard["readiness"] = data["readiness"]
            changes.append(f"Readiness: {data['readiness'].get('score', '?')}")

        dashboard["date"] = date.today().isoformat()

    dashboard_manager._read_and_write(updater)

    summary = "Oura data updated:\n" + "\n".join(f"  • {c}" for c in changes)
    logger.info(summary)
    return summary


# Module-level singleton
_client = None


def get_client() -> OuraClient:
    """Get singleton OuraClient."""
    global _client
    if _client is None:
        _client = OuraClient()
    return _client
