#!/usr/bin/env python3
"""
WhatsApp Dashboard Agent — Flask webhook server.
Receives Twilio WhatsApp messages, parses via Claude API,
updates dashboard-data.json, and responds via WhatsApp.

Includes: background scheduler for proactive nudges, data refresh,
weekly reports, and 'on this day' insights.
"""

import sys
import os
import logging
from datetime import datetime

# Ensure the agent directory is on the path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from flask import Flask, request, send_from_directory, jsonify
from twilio.twiml.messaging_response import MessagingResponse
from twilio.request_validator import RequestValidator

from config import Config
from claude_parser import parse_message, conversation_memory
from dashboard_manager import DashboardManager
from scheduler import DashboardScheduler
from health_connect import ingest_health_connect

# Dashboard static files directory (~/clawd/)
DASHBOARD_DIR = str(Config.DASHBOARD_DATA_PATH.parent if hasattr(Config.DASHBOARD_DATA_PATH, 'parent') else __import__('pathlib').Path(Config.DASHBOARD_DATA_PATH).parent)

# ── Logging ───────────────────────────────────────────────────

Config.LOG_DIR.mkdir(parents=True, exist_ok=True)
logging.basicConfig(
    level=getattr(logging, Config.LOG_LEVEL, logging.INFO),
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    handlers=[
        logging.FileHandler(Config.LOG_DIR / "whatsapp-agent.log"),
        logging.StreamHandler(),
    ],
)
logger = logging.getLogger("whatsapp-agent")

# ── App Setup ─────────────────────────────────────────────────

app = Flask(__name__)
dm = DashboardManager(Config.DASHBOARD_DATA_PATH)
scheduler = DashboardScheduler(dm)


@app.after_request
def add_no_cache_headers(response):
    """Prevent browser caching for JSON/API responses so dashboard always shows fresh data."""
    if (response.content_type and "json" in response.content_type) or request.path.startswith("/api/"):
        response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
        response.headers["Pragma"] = "no-cache"
        response.headers["Expires"] = "0"
    return response

# ── Startup banner (visible in logs to confirm restart) ──────
_VERSION = "2.4.0-scale-vision"  # Bump this when making changes
logger.info("=" * 60)
logger.info(f"  LUKE v{_VERSION} starting up")
logger.info(f"  Features: media-handler, conversation-memory, personality")
logger.info(f"  Context file: {'loaded' if hasattr(__import__('claude_parser', fromlist=['_BILL_CONTEXT']), '_BILL_CONTEXT') else 'missing'}")
logger.info("=" * 60)


# ── Helpers ───────────────────────────────────────────────────

def validate_twilio_request(req) -> bool:
    """Verify the request actually came from Twilio."""
    validator = RequestValidator(Config.TWILIO_AUTH_TOKEN)
    # Twilio sends the URL it called — use the Funnel URL
    url = req.url.replace("http://", "https://")
    signature = req.headers.get("X-Twilio-Signature", "")
    return validator.validate(url, req.form, signature)


def is_authorized(phone: str) -> bool:
    """Check if the sender is Bill's phone number."""
    # Normalize both sides: strip prefix, +, spaces, dashes
    import re
    clean = re.sub(r'[^0-9]', '', phone)  # Keep only digits
    expected = re.sub(r'[^0-9]', '', Config.BILL_PHONE_NUMBER)
    return clean == expected


# ── Quick commands (bypass Claude API for speed) ─────────────

QUICK_COMMANDS = {
    "briefing": lambda: scheduler.trigger_morning_briefing(),
    "morning": lambda: scheduler.trigger_morning_briefing(),
    "weekly": lambda: scheduler.trigger_weekly_report(),
    "weekly report": lambda: scheduler.trigger_weekly_report(),
    "report": lambda: scheduler.trigger_weekly_report(),
    "on this day": lambda: scheduler.get_on_this_day(),
    "otd": lambda: scheduler.get_on_this_day(),
    "history": lambda: scheduler.get_on_this_day(),
    "health": lambda: scheduler.trigger_health_ingest(),
    "ingest": lambda: scheduler.trigger_health_ingest(),
    "oura": lambda: scheduler.trigger_oura_ingest(),
    "oura ring": lambda: scheduler.trigger_oura_ingest(),
    "email": lambda: _get_email_summary(),
    "inbox": lambda: _get_email_summary(),
    "gmail": lambda: _get_email_summary(),
    "slate": lambda: _get_todays_slate(),
    "games": lambda: _get_todays_slate(),
    "games today": lambda: _get_todays_slate(),
    "tonight": lambda: _get_todays_slate(),
    "whats on": lambda: _get_todays_slate(),
    "what's on": lambda: _get_todays_slate(),
    "whats good": lambda: _get_best_targets(),
    "what's good": lambda: _get_best_targets(),
    "whats good today": lambda: _get_best_targets(),
    "what's good today": lambda: _get_best_targets(),
    "whats good tonight": lambda: _get_best_targets(),
    "what's good tonight": lambda: _get_best_targets(),
    # CRO anchor
    "cro anchor": lambda: dm.update_cro_anchor(),
    "reset anchor": lambda: dm.update_cro_anchor(),
    "anchor": lambda: dm.update_cro_anchor(),
    "reset cro": lambda: dm.update_cro_anchor(),
    # Spotify
    "now playing": lambda: _spotify("now_playing"),
    "what's playing": lambda: _spotify("now_playing"),
    "whats playing": lambda: _spotify("now_playing"),
    "recently played": lambda: _spotify("recent"),
    "recent": lambda: _spotify("recent"),
    "pause": lambda: _spotify("pause"),
    "stop music": lambda: _spotify("pause"),
    "resume": lambda: _spotify("resume"),
    "skip": lambda: _spotify("skip"),
    "next": lambda: _spotify("skip"),
    "next song": lambda: _spotify("skip"),
    "top artists": lambda: _spotify("top_artists"),
    "top tracks": lambda: _spotify("top_tracks"),
    "top songs": lambda: _spotify("top_tracks"),
    "new music": lambda: _spotify("new_releases"),
    "new releases": lambda: _spotify("new_releases"),
    "new drops": lambda: _spotify("new_releases"),
    "whats new": lambda: _spotify("new_releases"),
    "what's new": lambda: _spotify("new_releases"),
    "new music friday": lambda: _spotify("new_releases"),
}


def _spotify(action, query=None):
    """Quick command: Spotify control."""
    try:
        import spotify_module as sp
        if action == "now_playing":
            return sp.now_playing()
        elif action == "recent":
            return sp.recently_played()
        elif action == "pause":
            return sp.pause()
        elif action == "resume":
            return sp.resume()
        elif action == "skip":
            return sp.skip()
        elif action == "top_artists":
            return sp.top_artists()
        elif action == "top_tracks":
            return sp.top_tracks()
        elif action == "new_releases":
            return sp.new_releases()
        elif action == "play" and query:
            return sp.play_search(query)
        elif action == "queue" and query:
            return sp.add_to_queue(query)
        elif action == "playlist" and query:
            return sp.play_playlist(query)
        else:
            return "Not sure what to do with Spotify. Try 'now playing', 'pause', 'skip', or 'play [song]' — Luke"
    except Exception as e:
        return f"Spotify error: {str(e)[:100]} — Luke"


def _get_todays_slate():
    """Quick command: get today's sports slate."""
    try:
        from sports_betting import get_analyst
        analyst = get_analyst()
        return analyst.get_todays_slate()
    except Exception as e:
        return f"Sports module error: {str(e)[:100]} — Luke"


def _get_best_targets():
    """Quick command: get only the games that pass Bill's strategy filter."""
    try:
        from sports_betting import get_analyst
        analyst = get_analyst()
        return analyst.get_best_targets()
    except Exception as e:
        return f"Sports module error: {str(e)[:100]} — Luke"


def _get_email_summary():
    """Quick command: get Gmail briefing summary."""
    try:
        from gmail_briefing import get_gmail_briefing
        gmail = get_gmail_briefing()
        if not gmail.available:
            return "Gmail not connected yet. Run 'python setup_google_auth.py' to hook me up. — Luke"
        data = gmail.generate_briefing()
        if data.get("available"):
            return f"EMAIL SUMMARY\n\n{data.get('summary_text', 'No new emails')}"
        return "Couldn't fetch email data."
    except Exception as e:
        return f"Gmail error: {str(e)[:100]}"


def execute_action(parsed: dict) -> str:
    """Execute the parsed intent and return a response message."""
    intent = parsed.get("intent", "unknown")
    field = parsed.get("field", "unknown")
    values = parsed.get("values", {})
    response = parsed.get("response_text", "Done.")

    try:
        # ── Updates ───────────────────────────────────
        if intent == "update":
            if field == "weight":
                return dm.update_weight(float(values["value"]))
            elif field == "bodyFat":
                return dm.update_body_fat(float(values["value"]))
            elif field == "bloodPressure":
                return dm.update_blood_pressure(
                    int(values["systolic"]), int(values["diastolic"])
                )
            elif field == "focus":
                return dm.update_focus(values["text"])
            elif field == "crypto_anchor":
                price = values.get("value")
                if price is not None:
                    price = float(price)
                return dm.update_cro_anchor(price)

        # ── Additions ─────────────────────────────────
        elif intent == "add":
            if field == "schedule":
                return dm.add_event(
                    event_date=values.get("date", ""),
                    time=values.get("time", ""),
                    title=values.get("title", "Untitled"),
                    location=values.get("location", ""),
                    note=values.get("note", ""),
                    end_date=values.get("end_date"),
                )
            elif field == "signals":
                return dm.add_signal(
                    cat=values.get("cat", "ai"),
                    title=values.get("title", ""),
                    why=values.get("why", ""),
                    source=values.get("source", ""),
                )
            elif field == "supplements":
                return dm.add_supplement(
                    timing=values.get("timing", "morning"),
                    name=values.get("name", ""),
                    dose=values.get("dose", ""),
                    brand=values.get("brand", ""),
                    note=values.get("note", ""),
                )
            elif field == "viome":
                return dm.add_viome_food(
                    category=values.get("category", "avoid"),
                    food=values.get("food", ""),
                )

        # ── Removals ──────────────────────────────────
        elif intent == "remove":
            if field == "schedule":
                return dm.remove_event(
                    title_fragment=values.get("title_fragment", ""),
                    event_date=values.get("date"),
                )
            elif field == "supplements":
                return dm.remove_supplement(
                    name_fragment=values.get("name_fragment", "")
                )

        # ── Queries ───────────────────────────────────
        elif intent == "query":
            if field == "weight":
                return dm.query_weight()
            elif field == "bodyFat":
                return dm.query_body_fat()
            elif field == "bloodPressure":
                return dm.query_blood_pressure()
            elif field == "schedule":
                days = values.get("days_ahead", 14)
                return dm.query_schedule(days_ahead=int(days))
            elif field == "supplements":
                return dm.query_supplements()
            elif field == "signals":
                return dm.query_signals()
            elif field == "summary":
                return dm.query_summary()
            else:
                # For any query, return the Claude-generated response
                return response

        # ── Betting ───────────────────────────────────
        elif intent == "betting":
            try:
                from sports_betting import get_analyst
                analyst = get_analyst()
                bet_type = values.get("type", "analysis")
                team = values.get("team", "")

                if bet_type == "targets":
                    return analyst.get_best_targets()
                elif bet_type == "slate":
                    sport = values.get("sport")
                    return analyst.get_todays_slate(sport)
                elif team:
                    return analyst.generate_analysis(team)
                else:
                    return analyst.get_todays_slate()
            except Exception as e:
                logger.error(f"Betting handler error: {e}", exc_info=True)
                return response  # Fall back to Claude's response

        # ── Spotify ───────────────────────────────────
        elif intent == "spotify":
            action = values.get("action", "now_playing")
            query = values.get("query", "")
            return _spotify(action, query)

        # ── Module triggers (natural language → backend modules) ──
        elif intent == "module":
            module = values.get("module", "")
            MODULE_DISPATCH = {
                "oura": lambda: scheduler.trigger_oura_ingest(),
                "email": lambda: _get_email_summary(),
                "briefing": lambda: scheduler.trigger_morning_briefing(),
                "health": lambda: scheduler.trigger_health_ingest(),
                "slate": lambda: _get_todays_slate(),
                "weekly": lambda: scheduler.trigger_weekly_report(),
                "otd": lambda: scheduler.get_on_this_day(),
            }
            handler = MODULE_DISPATCH.get(module)
            if handler:
                try:
                    result = handler()
                    return result if result else f"Module '{module}' returned no data. — Luke"
                except Exception as e:
                    logger.error(f"Module '{module}' failed: {e}", exc_info=True)
                    return f"Hit a snag pulling {module} data: {str(e)[:100]} — Luke"
            else:
                logger.warning(f"Unknown module requested: {module}")
                return response

        # ── Chat (non-dashboard) ──────────────────────
        elif intent == "chat":
            return response

        # If we got here, the intent/field combo wasn't handled
        return response

    except KeyError as e:
        logger.error(f"Missing value for action: {e}")
        return f"Missing data for that update: {e}. Could you try again with more detail?"
    except Exception as e:
        logger.error(f"Action execution failed: {e}", exc_info=True)
        return f"Something went wrong: {str(e)[:100]}"


# ── Media Handling ────────────────────────────────────────────

def _handle_media_attachment(req, num_media, sender=""):
    """
    Handle incoming WhatsApp media attachments.
    - Images → scale screenshot detection (iHealth, etc.) via Claude Vision
    - Zip files → Health Connect ingestion
    Returns a response string, or None if the media wasn't handled.
    """
    import requests as http_requests
    import tempfile

    for i in range(num_media):
        media_url = req.form.get(f"MediaUrl{i}")
        content_type = req.form.get(f"MediaContentType{i}", "")

        logger.info(f"Media attachment {i}: type={content_type}, url={media_url}")

        # ── Handle images → health screenshot extraction (scale, BP, etc.) ──
        if content_type.lower().startswith("image/"):
            try:
                twilio_sid = Config.TWILIO_ACCOUNT_SID
                twilio_token = Config.TWILIO_AUTH_TOKEN
                img_resp = http_requests.get(
                    media_url, auth=(twilio_sid, twilio_token), timeout=30
                )
                img_resp.raise_for_status()
                image_data = img_resp.content
                logger.info(f"Downloaded image ({len(image_data) / 1024:.0f} KB)")

                from scale_vision import parse_health_screenshot, apply_scale_data, apply_bp_data

                # Single Claude Vision call — detects type + extracts in one shot
                extracted = parse_health_screenshot(image_data, content_type)
                data_type = extracted.get("type", "unknown")

                if data_type == "scale" and extracted.get("weight_lb"):
                    logger.info("Scale screenshot detected and parsed")
                    result = apply_scale_data(extracted, dm)
                    if sender:
                        conversation_memory.add_user_message(sender, "[Sent scale screenshot]")
                        conversation_memory.add_system_event(sender, result)
                    return f"Got it! 📊\n\n{result}\n\nDashboard updated. — Luke"

                elif data_type == "blood_pressure" and extracted.get("readings"):
                    logger.info(f"BP screenshot detected — {len(extracted['readings'])} readings")
                    result = apply_bp_data(extracted, dm)
                    if sender:
                        conversation_memory.add_user_message(sender, "[Sent blood pressure screenshot]")
                        conversation_memory.add_system_event(sender, result)
                    return f"Got it! 💓\n\n{result}\n\nDashboard updated. — Luke"

                elif data_type in ("scale", "blood_pressure"):
                    return (
                        "I can see that's a health screenshot but couldn't read "
                        "the numbers clearly. Try sending it again? — Luke"
                    )
                else:
                    logger.info(f"Image type '{data_type}' — not a health screenshot, passing through")
            except Exception as e:
                logger.error(f"Image processing failed: {e}", exc_info=True)
            # Don't return — let other handlers try if image wasn't health data
            continue

        # ── Handle zip files → Health Connect ingestion ──
        is_zip = (
            "zip" in content_type.lower()
            or "octet-stream" in content_type.lower()
        )

        if not is_zip:
            logger.info(f"Skipping unhandled media: {content_type}")
            continue

        try:
            # Download the file from Twilio (requires auth)
            twilio_sid = Config.TWILIO_ACCOUNT_SID
            twilio_token = Config.TWILIO_AUTH_TOKEN
            response = http_requests.get(
                media_url,
                auth=(twilio_sid, twilio_token),
                timeout=30,
            )
            response.raise_for_status()

            # Save to temp file
            temp_dir = tempfile.mkdtemp(prefix="clawd-whatsapp-")
            zip_path = os.path.join(
                temp_dir,
                f"health_connect_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
            )
            with open(zip_path, "wb") as f:
                f.write(response.content)

            size_kb = len(response.content) / 1024
            logger.info(f"Downloaded media to {zip_path} ({size_kb:.0f} KB)")

            # Validate it's actually a zip
            import zipfile
            if not zipfile.is_zipfile(zip_path):
                logger.warning(f"File is not a valid zip: {zip_path}")
                return "That file doesn't look like a valid zip. Send me a Health Connect export zip and I'll update your dashboard. — Luke"

            # Run the Health Connect ingestion
            summary = ingest_health_connect(zip_path, dm)

            # Archive the zip for reference
            archive_dir = Config.DASHBOARD_DATA_PATH.parent / "health" / datetime.now().strftime("%Y-%m-%d")
            archive_dir.mkdir(parents=True, exist_ok=True)
            import shutil
            shutil.copy2(zip_path, archive_dir / "health_connect_export.zip")

            # Cleanup temp
            shutil.rmtree(temp_dir, ignore_errors=True)

            result_msg = f"Got it! 📊\n\n{summary}\n\nDashboard updated. — Luke"

            # Record in conversation memory so Luke remembers this happened
            if sender:
                conversation_memory.add_user_message(sender, "[Sent Health Connect zip file]")
                conversation_memory.add_system_event(sender, summary)

            return result_msg

        except Exception as e:
            logger.error(f"Media processing failed: {e}", exc_info=True)
            return f"I received your file but hit an error processing it: {str(e)[:100]}. Try sending it again? — Luke"

    # No zip files found in the media
    return None


def _is_health_connect_mention(body: str) -> bool:
    """Detect if the message body is a Health Connect file reference."""
    b = body.lower().strip()
    # Twilio sends the filename as body when a document is shared
    return (
        ("health connect" in b and (".zip" in b or "export" in b))
        or b == "health connect.zip"
        or b == "health_connect.zip"
        or ("health" in b and "connect" in b and "zip" in b)
    )


def _handle_health_connect_text(sender: str) -> str:
    """
    Handle Health Connect file references detected in message body.
    Uses scheduler's comprehensive search: Drive API (72h) → multiple
    filesystem paths → health-imports → dashboard dir.
    """
    logger.info("Health Connect file detected in message text — triggering ingestion via scheduler")

    try:
        result = scheduler.trigger_health_ingest()
        if result and "No Health Connect" not in result:
            logger.info(f"Health Connect ingestion result: {result}")

            # Record in conversation memory
            if sender:
                conversation_memory.add_user_message(sender, "[Sent Health Connect zip via WhatsApp]")
                conversation_memory.add_system_event(sender, result)

            return f"Got it! 📊\n\n{result}\n\nDashboard updated. — Luke"
        else:
            logger.info(f"Scheduler ingest returned nothing useful: {result}")
    except Exception as e:
        logger.warning(f"Health Connect ingestion failed: {e}", exc_info=True)

    # Couldn't find the file — give helpful instructions
    return (
        "I see you're sharing a Health Connect export — nice! "
        "WhatsApp doesn't pass zip files through to me though. "
        "Your export should sync to Google Drive automatically — "
        "once it lands, just text me 'health' and I'll grab it. "
        "Or AirDrop it to ~/clawd/health-imports/ on your Mac. — Luke"
    )


# ── Routes ────────────────────────────────────────────────────

@app.route("/webhook/whatsapp", methods=["POST"])
def whatsapp_webhook():
    """Handle incoming WhatsApp messages from Twilio."""
    # Validate Twilio signature (skip in dev/testing)
    if Config.FLASK_HOST != "127.0.0.1" or os.getenv("VALIDATE_TWILIO", "true") == "true":
        if not validate_twilio_request(request):
            logger.warning("Invalid Twilio signature — rejecting request")
            return "Forbidden", 403

    sender = request.form.get("From", "")
    body = request.form.get("Body", "").strip()
    num_media_raw = request.form.get("NumMedia", "0")

    logger.info(f"Message from {sender}: body='{body}', NumMedia={num_media_raw}")
    # Log all form fields for debugging media issues
    if num_media_raw != "0":
        for key in request.form:
            if key.startswith("Media"):
                logger.info(f"  Twilio media field: {key}={request.form[key]}")

    # Authorization check
    if not is_authorized(sender):
        logger.warning(f"Unauthorized sender: {sender}")
        resp = MessagingResponse()
        resp.message("Luke only responds to Bill.")
        return str(resp), 200

    # ── Media attachment handling (Health Connect zip files) ──
    num_media = int(request.form.get("NumMedia", 0))
    if num_media > 0:
        media_result = _handle_media_attachment(request, num_media, sender=sender)
        if media_result:
            resp = MessagingResponse()
            resp.message(media_result)
            return str(resp), 200

    # ── Health Connect text detection ──
    # Twilio WhatsApp doesn't forward zip documents as media — it sends
    # the filename as body text instead. Detect this and trigger ingestion
    # via Google Drive or local filesystem.
    if body and _is_health_connect_mention(body):
        hc_result = _handle_health_connect_text(sender)
        resp = MessagingResponse()
        resp.message(hc_result)
        return str(resp), 200

    if not body:
        resp = MessagingResponse()
        resp.message("Empty message — send me anything and I'll handle it. — Luke")
        return str(resp), 200

    # Check for CRO anchor with specific price (e.g., "cro anchor 0.085")
    import re as _re_app
    cro_anchor_match = _re_app.match(r'^(?:cro\s+anchor|reset\s+anchor|anchor)\s+([\d.]+)$', body.lower().strip())
    if cro_anchor_match:
        try:
            anchor_price = float(cro_anchor_match.group(1))
            result = dm.update_cro_anchor(anchor_price)
        except (ValueError, Exception) as e:
            result = f"Couldn't parse that price. Try: 'cro anchor 0.085' — Luke"
        conversation_memory.add_user_message(sender, body)
        conversation_memory.add_assistant_message(sender, result)
        resp = MessagingResponse()
        resp.message(result)
        logger.info(f"CRO anchor command with price: {result[:80]}")
        return str(resp), 200

    # Check for quick commands first (no Claude API call needed)
    body_lower = body.lower().strip()
    if body_lower in QUICK_COMMANDS:
        try:
            result = QUICK_COMMANDS[body_lower]()
        except Exception as e:
            logger.error(f"Quick command '{body_lower}' failed: {e}", exc_info=True)
            result = f"Command '{body_lower}' hit an error: {str(e)[:120]} — Luke"
        if not result:
            result = f"Command '{body_lower}' returned no output. Check server logs. — Luke"
        # Record in conversation memory
        conversation_memory.add_user_message(sender, body)
        conversation_memory.add_assistant_message(sender, result)
        resp = MessagingResponse()
        resp.message(result)
        logger.info(f"Quick command '{body_lower}' executed: {str(result)[:80]}")
        return str(resp), 200

    # Read current dashboard state
    try:
        dashboard_data = dm.read()
    except Exception as e:
        logger.error(f"Failed to read dashboard data: {e}")
        resp = MessagingResponse()
        resp.message("Couldn't read dashboard data. Check the server logs.")
        return str(resp), 200

    # Parse intent via Claude (with conversation history)
    parsed = parse_message(body, dashboard_data, sender=sender)
    logger.info(f"Parsed intent: {parsed.get('intent')} / {parsed.get('field')} (confidence: {parsed.get('confidence')})")

    # Low confidence — ask for clarification
    if parsed.get("confidence") == "low" and parsed.get("intent") != "chat":
        resp = MessagingResponse()
        resp.message(parsed.get("response_text", "Could you rephrase that?"))
        return str(resp), 200

    # Execute the action
    result = execute_action(parsed)

    # Send response
    resp = MessagingResponse()
    resp.message(result)
    logger.info(f"Response sent: {result[:100]}")
    return str(resp), 200


@app.route("/health", methods=["GET"])
def health():
    """Health check endpoint with Google integration status."""
    google_status = {}
    try:
        from google_auth import is_configured
        google_status["auth_configured"] = is_configured()
    except Exception:
        google_status["auth_configured"] = False
    try:
        from gdrive_health import get_connector
        gdrive = get_connector()
        google_status["drive"] = gdrive.available
    except Exception:
        google_status["drive"] = False
    try:
        from gmail_briefing import get_gmail_briefing
        gmail = get_gmail_briefing()
        google_status["gmail"] = gmail.available
    except Exception:
        google_status["gmail"] = False

    oura_status = False
    try:
        from oura_api import get_client
        oura_status = get_client().available
    except Exception:
        pass

    spotify_status = False
    try:
        import spotify_module
        spotify_status = spotify_module.is_available()
    except Exception:
        pass

    return {
        "status": "ok",
        "scheduler": scheduler.running,
        "google": google_status,
        "oura": oura_status,
        "spotify": spotify_status,
        "timestamp": datetime.now().isoformat(),
    }, 200


@app.route("/api/briefing", methods=["GET"])
def api_briefing():
    """Return current briefing data for the dashboard."""
    briefing = scheduler.get_briefing_data()
    if briefing:
        return jsonify(briefing)
    return jsonify({"error": "No briefing data available yet"}), 404


# ── Static file serving (dashboard) ──────────────────────────

@app.route("/<path:filename>")
def serve_dashboard(filename):
    """Serve dashboard static files from ~/clawd/."""
    resp = send_from_directory(DASHBOARD_DIR, filename)
    # Prevent caching of JSON data files so dashboard always shows fresh data
    if filename.endswith(".json"):
        resp.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
        resp.headers["Pragma"] = "no-cache"
        resp.headers["Expires"] = "0"
    return resp


@app.route("/")
def serve_index():
    """Serve dashboard-standalone.html as the index."""
    return send_from_directory(DASHBOARD_DIR, "dashboard-standalone.html")


# ── Main ──────────────────────────────────────────────────────

if __name__ == "__main__":
    logger.info("Starting WhatsApp Dashboard Agent + Dashboard Server + Scheduler...")
    try:
        Config.validate()
    except (ValueError, FileNotFoundError) as e:
        logger.error(f"Config error: {e}")
        sys.exit(1)

    logger.info(f"Dashboard dir: {DASHBOARD_DIR}")
    logger.info(f"Dashboard data: {Config.DASHBOARD_DATA_PATH}")
    logger.info(f"Claude model: {Config.CLAUDE_MODEL}")

    # ── Step 1: Kill any leftover process on port 8080 BEFORE starting anything ──
    import subprocess, time as _time
    try:
        pids = subprocess.check_output(["lsof", "-ti", ":8080"], text=True).strip()
        for pid in pids.split("\n"):
            pid = pid.strip()
            if pid and pid != str(os.getpid()):
                logger.info(f"Killing leftover process on port 8080: PID {pid}")
                subprocess.run(["kill", "-9", pid], capture_output=True)
        logger.info("Waiting for port 8080 to release...")
        _time.sleep(5)
    except subprocess.CalledProcessError:
        pass  # No process on port — good

    # ── Step 2: Force SO_REUSEADDR on Flask's server ──
    from werkzeug.serving import BaseWSGIServer
    _original_init = BaseWSGIServer.__init__
    def _patched_init(self, *args, **kwargs):
        import http.server
        http.server.HTTPServer.allow_reuse_address = True
        self.allow_reuse_address = True
        _original_init(self, *args, **kwargs)
    BaseWSGIServer.__init__ = _patched_init

    # ── Step 3: Start the background scheduler AFTER port is secured ──
    logger.info(f"Listening on 0.0.0.0:8080")
    scheduler.start()
    logger.info("Background scheduler started (morning nudge, evening check-in, weekly report, data refresh)")

    app.run(host="0.0.0.0", port=8080, debug=False)
