"""
Fitness Tracker: Google Health API explorer (OAuth + data probe UI).
Single-user personal server; tokens stored in MySQL shop_stats.
"""
from __future__ import annotations

import json
import os
from datetime import date, datetime, timedelta, timezone
from typing import Any

import pymysql
import requests
from flask import Blueprint, jsonify, redirect, render_template, request, session, url_for

import config as _config

MYSQL_HOST = _config.MYSQL_HOST
MYSQL_PORT = _config.MYSQL_PORT
MYSQL_DATABASE = _config.MYSQL_DATABASE
MYSQL_USER = _config.MYSQL_USER
MYSQL_PASSWORD = _config.MYSQL_PASSWORD
GOOGLE_HEALTH_CLIENT_ID = getattr(_config, "GOOGLE_HEALTH_CLIENT_ID", "") or os.environ.get(
    "GOOGLE_HEALTH_CLIENT_ID", ""
)
GOOGLE_HEALTH_CLIENT_SECRET = getattr(
    _config, "GOOGLE_HEALTH_CLIENT_SECRET", ""
) or os.environ.get("GOOGLE_HEALTH_CLIENT_SECRET", "")
GOOGLE_HEALTH_REDIRECT_URI = getattr(
    _config,
    "GOOGLE_HEALTH_REDIRECT_URI",
    "",
) or os.environ.get(
    "GOOGLE_HEALTH_REDIRECT_URI",
    "http://192.168.50.5:5000/fitnesstracker/oauth/callback",
)

# Home-LAN OAuth uses http:// redirect URIs; allow token exchange over HTTP.
if str(GOOGLE_HEALTH_REDIRECT_URI).startswith("http://"):
    os.environ.setdefault("OAUTHLIB_INSECURE_TRANSPORT", "1")


def _google_libs():
    """Lazy-import Google OAuth libs so missing packages don't break app startup."""
    try:
        from google.auth.transport.requests import Request as GoogleAuthRequest
        from google.oauth2.credentials import Credentials
        from google_auth_oauthlib.flow import Flow
    except ImportError as exc:
        raise RuntimeError(
            "Google OAuth packages not installed. "
            "Run: pip install google-auth google-auth-oauthlib requests"
        ) from exc
    return GoogleAuthRequest, Credentials, Flow

bp = Blueprint("fitnesstracker", __name__)

HEALTH_API_BASE = "https://health.googleapis.com/v4"
TOKEN_URI = "https://oauth2.googleapis.com/token"
AUTH_URI = "https://accounts.google.com/o/oauth2/v2/auth"
REVOKE_URI = "https://oauth2.googleapis.com/revoke"

# Readonly scopes for the explorer. heart-rate needs health_metrics_and_measurements.
OAUTH_SCOPES = [
    "https://www.googleapis.com/auth/googlehealth.activity_and_fitness.readonly",
    "https://www.googleapis.com/auth/googlehealth.health_metrics_and_measurements.readonly",
    "https://www.googleapis.com/auth/googlehealth.sleep.readonly",
    "https://www.googleapis.com/auth/googlehealth.profile.readonly",
    "https://www.googleapis.com/auth/googlehealth.settings.readonly",
    "openid",
    "https://www.googleapis.com/auth/userinfo.email",
    "https://www.googleapis.com/auth/userinfo.profile",
]

# Explorer catalog: dataType path id, record kind, preferred fetch method, filter field stem.
DATA_TYPES = [
    {
        "id": "steps",
        "label": "Steps",
        "kind": "interval",
        "method": "dailyRollUp",
        "filter_stem": "steps",
        "value_keys": ["count", "countSum"],
    },
    {
        "id": "distance",
        "label": "Distance",
        "kind": "interval",
        "method": "dailyRollUp",
        "filter_stem": "distance",
        "value_keys": ["distanceMillimeters", "distanceMillimetersSum"],
    },
    {
        "id": "active-minutes",
        "label": "Active Minutes",
        "kind": "interval",
        "method": "dailyRollUp",
        "filter_stem": "active_minutes",
        "value_keys": ["minutes", "minutesSum"],
    },
    {
        "id": "active-energy-burned",
        "label": "Active Energy Burned",
        "kind": "interval",
        "method": "dailyRollUp",
        "filter_stem": "active_energy_burned",
        "value_keys": ["energyKilocalories", "energyKilocaloriesSum"],
    },
    {
        "id": "total-calories",
        "label": "Total Calories",
        "kind": "interval",
        "method": "dailyRollUp",
        "filter_stem": "total_calories",
        "value_keys": ["energyKilocalories", "energyKilocaloriesSum"],
    },
    {
        "id": "floors",
        "label": "Floors",
        "kind": "interval",
        "method": "dailyRollUp",
        "filter_stem": "floors",
        "value_keys": ["floors", "floorsSum"],
    },
    {
        "id": "active-zone-minutes",
        "label": "Active Zone Minutes",
        "kind": "interval",
        "method": "dailyRollUp",
        "filter_stem": "active_zone_minutes",
        "value_keys": ["totalMinutes", "totalMinutesSum", "fatBurnMinutesSum"],
    },
    {
        "id": "heart-rate",
        "label": "Heart Rate",
        "kind": "sample",
        "method": "list",
        "filter_stem": "heart_rate",
        "value_keys": ["beatsPerMinute", "beatsPerMinuteAvg"],
    },
    {
        "id": "daily-resting-heart-rate",
        "label": "Daily Resting Heart Rate",
        "kind": "daily",
        "method": "list",
        "filter_stem": "daily_resting_heart_rate",
        "value_keys": ["beatsPerMinute"],
    },
    {
        "id": "sleep",
        "label": "Sleep",
        "kind": "session",
        "method": "list",
        "filter_stem": "sleep",
        "value_keys": ["minutesAsleep", "minutesInBed"],
    },
    {
        "id": "exercise",
        "label": "Exercise",
        "kind": "session",
        "method": "list",
        "filter_stem": "exercise",
        "value_keys": ["activityName", "calories"],
    },
    {
        "id": "weight",
        "label": "Weight",
        "kind": "sample",
        "method": "list",
        "filter_stem": "weight",
        "value_keys": ["weightKilograms"],
    },
]

_DATA_TYPE_BY_ID = {d["id"]: d for d in DATA_TYPES}


def _get_conn():
    return pymysql.connect(
        host=MYSQL_HOST,
        port=MYSQL_PORT,
        user=MYSQL_USER,
        password=MYSQL_PASSWORD,
        database=MYSQL_DATABASE,
        cursorclass=pymysql.cursors.DictCursor,
        autocommit=True,
    )


def ensure_fitness_tables(conn=None):
    own = conn is None
    if own:
        conn = _get_conn()
    try:
        with conn.cursor() as cur:
            cur.execute(
                """
                CREATE TABLE IF NOT EXISTS fitness_oauth_tokens (
                    id TINYINT NOT NULL PRIMARY KEY,
                    refresh_token TEXT,
                    access_token TEXT,
                    token_expiry DATETIME NULL,
                    scopes TEXT,
                    email VARCHAR(255) NULL,
                    profile_json MEDIUMTEXT NULL,
                    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
                        ON UPDATE CURRENT_TIMESTAMP
                ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
                """
            )
    finally:
        if own:
            conn.close()


def _client_configured() -> bool:
    return bool(GOOGLE_HEALTH_CLIENT_ID and GOOGLE_HEALTH_CLIENT_SECRET)


def _oauth_client_config() -> dict:
    return {
        "web": {
            "client_id": GOOGLE_HEALTH_CLIENT_ID,
            "client_secret": GOOGLE_HEALTH_CLIENT_SECRET,
            "auth_uri": AUTH_URI,
            "token_uri": TOKEN_URI,
            "redirect_uris": [GOOGLE_HEALTH_REDIRECT_URI],
        }
    }


def _load_token_row() -> dict | None:
    ensure_fitness_tables()
    conn = _get_conn()
    try:
        with conn.cursor() as cur:
            cur.execute("SELECT * FROM fitness_oauth_tokens WHERE id = 1")
            return cur.fetchone()
    finally:
        conn.close()


def _save_tokens(
    *,
    refresh_token: str | None = None,
    access_token: str | None = None,
    expiry: datetime | None = None,
    scopes: list[str] | None = None,
    email: str | None = None,
    profile_json: str | None = None,
):
    """Upsert the single personal token row. None args keep existing values."""
    ensure_fitness_tables()
    row = _load_token_row() or {}

    if refresh_token is None:
        refresh_token = row.get("refresh_token")
    if access_token is None:
        access_token = row.get("access_token")
    if email is None:
        email = row.get("email")
    if profile_json is None:
        profile_json = row.get("profile_json")

    if scopes is not None:
        scopes_str = " ".join(scopes)
    else:
        scopes_str = row.get("scopes") or ""

    if expiry is not None:
        if expiry.tzinfo is not None:
            expiry_naive = expiry.astimezone(timezone.utc).replace(tzinfo=None)
        else:
            expiry_naive = expiry
    else:
        expiry_naive = row.get("token_expiry")

    conn = _get_conn()
    try:
        with conn.cursor() as cur:
            cur.execute(
                """
                INSERT INTO fitness_oauth_tokens
                    (id, refresh_token, access_token, token_expiry, scopes, email, profile_json)
                VALUES
                    (1, %s, %s, %s, %s, %s, %s)
                ON DUPLICATE KEY UPDATE
                    refresh_token = VALUES(refresh_token),
                    access_token = VALUES(access_token),
                    token_expiry = VALUES(token_expiry),
                    scopes = VALUES(scopes),
                    email = VALUES(email),
                    profile_json = VALUES(profile_json)
                """,
                (
                    refresh_token,
                    access_token,
                    expiry_naive,
                    scopes_str,
                    email,
                    profile_json,
                ),
            )
    finally:
        conn.close()


def _clear_tokens():
    ensure_fitness_tables()
    conn = _get_conn()
    try:
        with conn.cursor() as cur:
            cur.execute("DELETE FROM fitness_oauth_tokens WHERE id = 1")
    finally:
        conn.close()


def _credentials_from_row(row: dict):
    if not row or not row.get("refresh_token"):
        return None
    _, Credentials, _ = _google_libs()
    expiry = row.get("token_expiry")
    if isinstance(expiry, datetime) and expiry.tzinfo is None:
        expiry = expiry.replace(tzinfo=timezone.utc)
    scopes = (row.get("scopes") or "").split()
    return Credentials(
        token=row.get("access_token"),
        refresh_token=row.get("refresh_token"),
        token_uri=TOKEN_URI,
        client_id=GOOGLE_HEALTH_CLIENT_ID,
        client_secret=GOOGLE_HEALTH_CLIENT_SECRET,
        scopes=scopes or OAUTH_SCOPES,
        expiry=expiry,
    )


def _get_valid_credentials():
    if not _client_configured():
        return None
    row = _load_token_row()
    creds = _credentials_from_row(row) if row else None
    if not creds:
        return None
    if not creds.valid:
        if creds.expired and creds.refresh_token:
            try:
                GoogleAuthRequest, _, _ = _google_libs()
                creds.refresh(GoogleAuthRequest())
            except Exception:
                return None
            _save_tokens(
                refresh_token=creds.refresh_token,
                access_token=creds.token,
                expiry=creds.expiry,
                scopes=list(creds.scopes or []),
            )
        else:
            return None
    return creds


def _health_request(
    method: str,
    path: str,
    *,
    params: dict | None = None,
    json_body: dict | None = None,
) -> tuple[dict | list | None, int, str | None]:
    creds = _get_valid_credentials()
    if not creds or not creds.token:
        return None, 401, "Not connected to Google Health"
    url = f"{HEALTH_API_BASE}/{path.lstrip('/')}"
    headers = {
        "Authorization": f"Bearer {creds.token}",
        "Accept": "application/json",
    }
    try:
        resp = requests.request(
            method,
            url,
            headers=headers,
            params=params,
            json=json_body,
            timeout=60,
        )
    except requests.RequestException as exc:
        return None, 502, f"Upstream request failed: {exc}"

    if resp.status_code == 204 or not resp.content:
        return {}, resp.status_code, None

    try:
        data = resp.json()
    except ValueError:
        return None, resp.status_code, resp.text[:500] or "Non-JSON response"

    if resp.status_code >= 400:
        err = data.get("error", {}) if isinstance(data, dict) else {}
        message = err.get("message") if isinstance(err, dict) else None
        if not message:
            message = resp.text[:500] or f"HTTP {resp.status_code}"
        return data, resp.status_code, message
    return data, resp.status_code, None


def _civil_datetime(d: date, hours=0, minutes=0, seconds=0) -> dict:
    return {
        "date": {"year": d.year, "month": d.month, "day": d.day},
        "time": {"hours": hours, "minutes": minutes, "seconds": seconds, "nanos": 0},
    }


def _parse_date(value: str | None, default: date) -> date:
    if not value:
        return default
    try:
        return date.fromisoformat(value[:10])
    except ValueError:
        return default


def _camel_to_snake_stem(data_type_id: str) -> str:
    # heart-rate -> heart_rate; daily-resting-heart-rate -> daily_resting_heart_rate
    return data_type_id.replace("-", "_")


def _build_list_filter(meta: dict, start: date, end_exclusive: date) -> str:
    stem = meta.get("filter_stem") or _camel_to_snake_stem(meta["id"])
    kind = meta["kind"]
    start_s = start.isoformat()
    end_s = end_exclusive.isoformat()
    if kind == "interval":
        return (
            f'{stem}.interval.civil_start_time >= "{start_s}" AND '
            f'{stem}.interval.civil_start_time < "{end_s}"'
        )
    if kind == "sample":
        # Prefer civil when available; physical time works for all samples.
        start_z = f"{start_s}T00:00:00Z"
        end_z = f"{end_s}T00:00:00Z"
        return (
            f'{stem}.sample_time.physical_time >= "{start_z}" AND '
            f'{stem}.sample_time.physical_time < "{end_z}"'
        )
    if kind == "daily":
        return f'{stem}.date >= "{start_s}" AND {stem}.date < "{end_s}"'
    if meta["id"] == "sleep":
        start_z = f"{start_s}T00:00:00Z"
        end_z = f"{end_s}T00:00:00Z"
        return (
            f'{stem}.interval.end_time >= "{start_z}" AND '
            f'{stem}.interval.end_time < "{end_z}"'
        )
    # exercise and other sessions (excluding sleep)
    return (
        f'{stem}.interval.civil_start_time >= "{start_s}" AND '
        f'{stem}.interval.civil_start_time < "{end_s}"'
    )


def _first_numeric(obj: Any, keys: list[str]) -> float | None:
    if not isinstance(obj, dict):
        return None
    for key in keys:
        if key in obj and obj[key] is not None:
            try:
                return float(obj[key])
            except (TypeError, ValueError):
                continue
    for value in obj.values():
        if isinstance(value, (int, float)) and not isinstance(value, bool):
            return float(value)
        if isinstance(value, str):
            try:
                return float(value)
            except ValueError:
                continue
        if isinstance(value, dict):
            nested = _first_numeric(value, keys)
            if nested is not None:
                return nested
    return None


def _civil_label(civil: dict | None) -> str | None:
    if not isinstance(civil, dict):
        return None
    d = civil.get("date") or {}
    try:
        y, m, day = int(d["year"]), int(d["month"]), int(d["day"])
        return f"{y:04d}-{m:02d}-{day:02d}"
    except (KeyError, TypeError, ValueError):
        return None


def _payload_for_type(point: dict, data_type_id: str) -> dict | None:
    # REST JSON uses camelCase for union fields (heartRate, activeEnergyBurned, …).
    camel = "".join(
        part.capitalize() if i else part
        for i, part in enumerate(data_type_id.split("-"))
    )
    # Fix first segment lower: ActiveEnergyBurned -> activeEnergyBurned
    if camel and camel[0].isupper():
        camel = camel[0].lower() + camel[1:]
    snake = data_type_id.replace("-", "_")
    for key in (camel, snake, data_type_id):
        if key in point and isinstance(point[key], dict):
            return point[key]
    # Fallback: any non-metadata object field
    skip = {
        "name",
        "dataSource",
        "interval",
        "sampleTime",
        "date",
        "civilStartTime",
        "civilEndTime",
        "createTime",
        "updateTime",
    }
    for key, value in point.items():
        if key in skip:
            continue
        if isinstance(value, dict):
            return value
    return None


def _normalize_series(meta: dict, raw: dict, method: str) -> dict:
    labels: list[str] = []
    values: list[float | None] = []
    points_out: list[dict] = []
    value_keys = meta.get("value_keys") or []

    if method == "dailyRollUp":
        for item in raw.get("rollupDataPoints") or []:
            label = _civil_label(item.get("civilStartTime")) or "?"
            payload = _payload_for_type(item, meta["id"]) or {}
            num = _first_numeric(payload, value_keys)
            labels.append(label)
            values.append(num)
            points_out.append({"label": label, "value": num, "payload": payload})
    else:
        for item in raw.get("dataPoints") or []:
            payload = _payload_for_type(item, meta["id"]) or {}
            label = None
            interval = item.get("interval") or {}
            sample = item.get("sampleTime") or {}
            label = (
                _civil_label(interval.get("civilStartTime"))
                or interval.get("startTime")
                or _civil_label(sample.get("civilTime"))
                or sample.get("physicalTime")
                or _civil_label({"date": item.get("date")})
                or item.get("name")
                or "?"
            )
            if isinstance(item.get("date"), dict):
                label = _civil_label({"date": item["date"]}) or label
            num = _first_numeric(payload, value_keys)
            labels.append(str(label))
            values.append(num)
            points_out.append({"label": str(label), "value": num, "payload": payload})

    numeric = [v for v in values if v is not None]
    summary = {
        "count": len(points_out),
        "numericCount": len(numeric),
        "sum": sum(numeric) if numeric else None,
        "avg": (sum(numeric) / len(numeric)) if numeric else None,
        "min": min(numeric) if numeric else None,
        "max": max(numeric) if numeric else None,
        "latest": numeric[0] if numeric else None,
    }
    return {
        "labels": labels,
        "values": values,
        "points": points_out,
        "summary": summary,
        "chartType": "bar" if method == "dailyRollUp" else "line",
    }


@bp.route("/")
def index():
    ensure_fitness_tables()
    return render_template(
        "fitnesstracker/index.html",
        data_types=DATA_TYPES,
        client_configured=_client_configured(),
        redirect_uri=GOOGLE_HEALTH_REDIRECT_URI,
    )


@bp.route("/oauth/start")
def oauth_start():
    if not _client_configured():
        return (
            "Google Health OAuth is not configured. "
            "Set GOOGLE_HEALTH_CLIENT_ID and GOOGLE_HEALTH_CLIENT_SECRET in config.env.",
            500,
        )
    try:
        _, _, Flow = _google_libs()
    except RuntimeError as exc:
        return str(exc), 500
    flow = Flow.from_client_config(
        _oauth_client_config(),
        scopes=OAUTH_SCOPES,
        redirect_uri=GOOGLE_HEALTH_REDIRECT_URI,
    )
    auth_url, state = flow.authorization_url(
        access_type="offline",
        include_granted_scopes="true",
        prompt="consent",
    )
    session["fitness_oauth_state"] = state
    return redirect(auth_url)


@bp.route("/oauth/callback")
def oauth_callback():
    if not _client_configured():
        return "OAuth not configured", 500
    err = request.args.get("error")
    if err:
        return f"Google OAuth error: {err}", 400
    state = session.pop("fitness_oauth_state", None)
    if not state or state != request.args.get("state"):
        return "Invalid OAuth state", 400

    try:
        _, _, Flow = _google_libs()
    except RuntimeError as exc:
        return str(exc), 500
    flow = Flow.from_client_config(
        _oauth_client_config(),
        scopes=OAUTH_SCOPES,
        state=state,
        redirect_uri=GOOGLE_HEALTH_REDIRECT_URI,
    )
    try:
        flow.fetch_token(authorization_response=request.url)
    except Exception as exc:
        return f"Token exchange failed: {exc}", 400

    creds = flow.credentials
    email = None
    try:
        info = requests.get(
            "https://www.googleapis.com/oauth2/v2/userinfo",
            headers={"Authorization": f"Bearer {creds.token}"},
            timeout=30,
        )
        if info.ok:
            email = info.json().get("email")
    except requests.RequestException:
        pass

    profile_snippet = None
    try:
        pref = requests.get(
            f"{HEALTH_API_BASE}/users/me/profile",
            headers={"Authorization": f"Bearer {creds.token}", "Accept": "application/json"},
            timeout=30,
        )
        if pref.ok:
            profile_snippet = pref.text
    except requests.RequestException:
        pass

    _save_tokens(
        refresh_token=creds.refresh_token,
        access_token=creds.token,
        expiry=creds.expiry,
        scopes=list(creds.scopes or OAUTH_SCOPES),
        email=email,
        profile_json=profile_snippet,
    )
    return redirect(url_for("fitnesstracker.index"))


@bp.route("/oauth/disconnect", methods=["POST"])
def oauth_disconnect():
    row = _load_token_row()
    token = None
    if row:
        token = row.get("refresh_token") or row.get("access_token")
    if token:
        try:
            requests.post(REVOKE_URI, data={"token": token}, timeout=20)
        except requests.RequestException:
            pass
    _clear_tokens()
    return jsonify({"ok": True})


@bp.route("/api/status")
def api_status():
    ensure_fitness_tables()
    configured = _client_configured()
    row = _load_token_row()
    connected = bool(row and row.get("refresh_token"))
    creds_ok = False
    if connected:
        creds_ok = _get_valid_credentials() is not None
    profile = None
    if row and row.get("profile_json"):
        try:
            profile = json.loads(row["profile_json"])
        except (TypeError, ValueError):
            profile = None
    return jsonify(
        {
            "configured": configured,
            "connected": connected,
            "credentialsValid": creds_ok,
            "email": row.get("email") if row else None,
            "scopes": (row.get("scopes") or "").split() if row else [],
            "updatedAt": row.get("updated_at").isoformat(sep=" ")
            if row and row.get("updated_at")
            else None,
            "redirectUri": GOOGLE_HEALTH_REDIRECT_URI,
            "profile": profile,
            "dataTypes": [
                {"id": d["id"], "label": d["label"], "method": d["method"], "kind": d["kind"]}
                for d in DATA_TYPES
            ],
        }
    )


@bp.route("/api/profile")
def api_profile():
    data, status, err = _health_request("GET", "users/me/profile")
    if err:
        return jsonify({"ok": False, "error": err, "raw": data}), status
    # Cache a copy on the token row for status display.
    try:
        _save_tokens(
            refresh_token=None,
            access_token=None,
            expiry=None,
            scopes=None,
            profile_json=json.dumps(data),
        )
    except Exception:
        pass
    return jsonify({"ok": True, "data": data})


@bp.route("/api/settings")
def api_settings():
    data, status, err = _health_request("GET", "users/me/settings")
    if err:
        return jsonify({"ok": False, "error": err, "raw": data}), status
    return jsonify({"ok": True, "data": data})


@bp.route("/api/identity")
def api_identity():
    data, status, err = _health_request("GET", "users/me/identity")
    if err:
        return jsonify({"ok": False, "error": err, "raw": data}), status
    return jsonify({"ok": True, "data": data})


@bp.route("/api/explore")
def api_explore():
    data_type = (request.args.get("dataType") or "steps").strip()
    meta = _DATA_TYPE_BY_ID.get(data_type)
    if not meta:
        return jsonify({"ok": False, "error": f"Unknown dataType: {data_type}"}), 400

    today = date.today()
    default_start = today - timedelta(days=7)
    start = _parse_date(request.args.get("start"), default_start)
    end_inclusive = _parse_date(request.args.get("end"), today)
    if end_inclusive < start:
        start, end_inclusive = end_inclusive, start
    # Closed-open end: day after inclusive end.
    end_exclusive = end_inclusive + timedelta(days=1)

    method = (request.args.get("method") or meta["method"]).strip()
    if method not in ("list", "dailyRollUp"):
        return jsonify({"ok": False, "error": "method must be list or dailyRollUp"}), 400

    # Enforce documented rollup max ranges.
    span_days = (end_exclusive - start).days
    max_days = 14 if data_type in {
        "calories-in-heart-rate-zone",
        "heart-rate",
        "active-minutes",
        "total-calories",
    } else 90
    if method == "dailyRollUp" and span_days > max_days:
        return jsonify(
            {
                "ok": False,
                "error": f"dailyRollUp max range for {data_type} is {max_days} days",
            }
        ), 400

    parent = f"users/me/dataTypes/{data_type}"
    raw = None
    status = 200
    err = None
    request_info: dict[str, Any] = {"method": method, "parent": parent}

    if method == "list":
        filt = _build_list_filter(meta, start, end_exclusive)
        params = {"filter": filt, "pageSize": 1000}
        request_info["params"] = params
        raw, status, err = _health_request(
            "GET", f"{parent}/dataPoints", params=params
        )
    else:
        body = {
            "range": {
                "start": _civil_datetime(start),
                "end": _civil_datetime(end_exclusive),
            },
            "windowSizeDays": 1,
            "pageSize": 1000,
        }
        request_info["body"] = body
        raw, status, err = _health_request(
            "POST", f"{parent}/dataPoints:dailyRollUp", json_body=body
        )

    if err:
        return jsonify(
            {
                "ok": False,
                "error": err,
                "request": request_info,
                "raw": raw,
                "dataType": data_type,
            }
        ), status

    series = _normalize_series(meta, raw or {}, method)
    return jsonify(
        {
            "ok": True,
            "dataType": data_type,
            "label": meta["label"],
            "start": start.isoformat(),
            "end": end_inclusive.isoformat(),
            "method": method,
            "request": request_info,
            "series": series,
            "raw": raw,
        }
    )


@bp.route("/api/health")
def api_health():
    return jsonify(
        {
            "ok": True,
            "service": "fitnesstracker",
            "configured": _client_configured(),
        }
    )
