from __future__ import annotations

import json
import math
import sqlite3
from dataclasses import asdict, dataclass
from datetime import datetime, timezone
import os
from functools import wraps
from pathlib import Path
from statistics import median
from typing import List, Optional
try:
    from zoneinfo import ZoneInfo, ZoneInfoNotFoundError  # Python 3.9+
except ImportError:  # pragma: no cover
    from backports.zoneinfo import ZoneInfo, ZoneInfoNotFoundError  # type: ignore

from flask import Flask, jsonify, redirect, render_template, request, session, url_for

BASE_DIR = Path(__file__).resolve().parent
DB_PATH = BASE_DIR / "db.sqlite3"
try:
    ET = ZoneInfo("America/New_York")
except ZoneInfoNotFoundError:
    # Fallback if tzdata is missing; prefer to install tzdata for correct offsets.
    ET = timezone.utc

app = Flask(__name__, template_folder="templates", static_folder="static")
app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "change-this-secret")

APP_USER = os.environ.get("FASTING_USER", "admin")
APP_PASS = os.environ.get("FASTING_PASS", "password")


# ---- Data layer --------------------------------------------------------------


def get_conn() -> sqlite3.Connection:
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    return conn


def init_db() -> None:
    with get_conn() as conn:
        conn.execute(
            """
            CREATE TABLE IF NOT EXISTS fasts (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                start_utc TEXT NOT NULL,
                end_utc TEXT,
                goal_hours REAL NOT NULL,
                note TEXT,
                created_at TEXT NOT NULL
            )
            """
        )


def to_iso(dt: datetime) -> str:
    """Return an ISO string with timezone."""
    return dt.astimezone(timezone.utc).isoformat()


def from_iso(value: str) -> datetime:
    return datetime.fromisoformat(value).astimezone(timezone.utc)


# ---- Models -----------------------------------------------------------------


@dataclass
class Fast:
    id: int
    start_utc: datetime
    end_utc: Optional[datetime]
    goal_hours: float
    note: Optional[str]

    @property
    def duration_seconds(self) -> Optional[float]:
        if not self.end_utc:
            return None
        return (self.end_utc - self.start_utc).total_seconds()

    def as_json(self) -> dict:
        return {
            "id": self.id,
            "start_utc": self.start_utc.isoformat(),
            "end_utc": self.end_utc.isoformat() if self.end_utc else None,
            "goal_hours": self.goal_hours,
            "note": self.note,
        }


def row_to_fast(row: sqlite3.Row) -> Fast:
    return Fast(
        id=row["id"],
        start_utc=from_iso(row["start_utc"]),
        end_utc=from_iso(row["end_utc"]) if row["end_utc"] else None,
        goal_hours=float(row["goal_hours"]),
        note=row["note"],
    )


# ---- Service helpers --------------------------------------------------------


def fetch_active_fast(conn: sqlite3.Connection) -> Optional[Fast]:
    row = conn.execute(
        "SELECT * FROM fasts WHERE end_utc IS NULL ORDER BY start_utc DESC LIMIT 1"
    ).fetchone()
    return row_to_fast(row) if row else None


def fetch_completed_fasts(conn: sqlite3.Connection, limit: int = 50) -> List[Fast]:
    rows = conn.execute(
        "SELECT * FROM fasts WHERE end_utc IS NOT NULL ORDER BY end_utc DESC LIMIT ?",
        (limit,),
    ).fetchall()
    return [row_to_fast(r) for r in rows]


def create_fast(
    conn: sqlite3.Connection, start_utc: datetime, goal_hours: float, note: str | None
) -> Fast:
    created_at = to_iso(datetime.now(timezone.utc))
    cur = conn.execute(
        """
        INSERT INTO fasts (start_utc, end_utc, goal_hours, note, created_at)
        VALUES (?, NULL, ?, ?, ?)
        """,
        (to_iso(start_utc), goal_hours, note, created_at),
    )
    fast_id = cur.lastrowid
    row = conn.execute("SELECT * FROM fasts WHERE id = ?", (fast_id,)).fetchone()
    return row_to_fast(row)


def end_active_fast(conn: sqlite3.Connection, end_utc: datetime) -> Optional[Fast]:
    active = fetch_active_fast(conn)
    if not active:
        return None
    conn.execute(
        "UPDATE fasts SET end_utc = ? WHERE id = ?", (to_iso(end_utc), active.id)
    )
    row = conn.execute("SELECT * FROM fasts WHERE id = ?", (active.id,)).fetchone()
    return row_to_fast(row)


def longest_streak(completed: List[Fast]) -> int:
    if not completed:
        return 0
    # Use end date in ET for streak calculations.
    days = sorted(
        {f.end_utc.astimezone(ET).date() for f in completed if f.end_utc}
    )
    best = streak = 1
    for i in range(1, len(days)):
        if (days[i] - days[i - 1]).days == 1:
            streak += 1
        else:
            best = max(best, streak)
            streak = 1
    best = max(best, streak)
    return best


def summarize_stats(completed: List[Fast]) -> dict:
    durations = [
        (f.end_utc - f.start_utc).total_seconds() / 3600 for f in completed if f.end_utc
    ]
    if durations:
        avg = sum(durations) / len(durations)
        med = median(durations)
    else:
        avg = med = 0
    return {
        "average_hours": round(avg, 2),
        "median_hours": round(med, 2),
        "longest_streak": longest_streak(completed),
        "completed_count": len(durations),
    }


def format_history(completed: List[Fast], limit: int = 10) -> List[dict]:
    items = []
    for fast in completed[:limit]:
        end = fast.end_utc or datetime.now(timezone.utc)
        duration_hours = round((end - fast.start_utc).total_seconds() / 3600, 2)
        items.append(
            {
                "id": fast.id,
                "start_et": fast.start_utc.astimezone(ET).isoformat(),
                "end_et": end.astimezone(ET).isoformat(),
                "duration_hours": duration_hours,
                "goal_hours": fast.goal_hours,
                "note": fast.note,
            }
        )
    return items


# ---- Auth helpers -----------------------------------------------------------


def login_required(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        from flask import session

        if session.get("auth"):
            return fn(*args, **kwargs)
        return redirect(url_for("login", next=request.path))

    return wrapper


# ---- Routes -----------------------------------------------------------------


@app.route("/")
@login_required
def index():
    init_db()
    with get_conn() as conn:
        active = fetch_active_fast(conn)
        completed = fetch_completed_fasts(conn, limit=50)
        stats = summarize_stats(completed)
        history = format_history(completed, limit=10)
    return render_template(
        "index.html",
        active=active.as_json() if active else None,
        stats=stats,
        history=history,
        tz_name="America/New_York",
    )


def parse_start_time(value: str | None) -> datetime:
    if value:
        # Expect an ISO string without timezone from <input type="datetime-local">.
        naive = datetime.fromisoformat(value)
        aware = naive.replace(tzinfo=ET)
        return aware.astimezone(timezone.utc)
    # Default to now in ET.
    return datetime.now(ET).astimezone(timezone.utc)


@app.route("/fasts/start", methods=["POST"])
@login_required
def start_fast():
    init_db()
    start_time_raw = request.form.get("start_time") or None
    goal_hours_raw = request.form.get("goal_hours") or "16"
    note = request.form.get("note") or None

    try:
        goal_hours = float(goal_hours_raw)
        start_utc = parse_start_time(start_time_raw)
    except Exception as exc:  # broad to keep UX simple
        return jsonify({"status": "error", "message": f"Invalid input: {exc}"}), 400

    with get_conn() as conn:
        # Close any existing active fast to avoid overlaps.
        end_active_fast(conn, datetime.now(timezone.utc))
        new_fast = create_fast(conn, start_utc, goal_hours, note)
        completed = fetch_completed_fasts(conn, limit=50)
        stats = summarize_stats(completed)
        history = format_history(completed)

    return jsonify(
        {
            "status": "started",
            "active_fast": new_fast.as_json(),
            "stats": stats,
            "history": history,
        }
    )


@app.route("/fasts/reset", methods=["POST"])
@login_required
def reset_fast():
    init_db()
    with get_conn() as conn:
        ended = end_active_fast(conn, datetime.now(timezone.utc))
        completed = fetch_completed_fasts(conn, limit=50)
        stats = summarize_stats(completed)
        history = format_history(completed)
    return jsonify(
        {
            "status": "reset",
            "ended_fast": ended.as_json() if ended else None,
            "active_fast": None,
            "stats": stats,
            "history": history,
        }
    )


@app.route("/fasts/clear", methods=["POST"])
@login_required
def clear_history():
    """Delete all fast records (active and historical)."""
    init_db()
    with get_conn() as conn:
        conn.execute("DELETE FROM fasts")
    return jsonify(
        {
            "status": "cleared",
            "active_fast": None,
            "stats": summarize_stats([]),
            "history": [],
        }
    )


@app.route("/health")
def health():
    return {"ok": True}


@app.route("/login", methods=["GET", "POST"])
def login():
    error = None
    if request.method == "POST":
        username = request.form.get("username", "")
        password = request.form.get("password", "")
        if username == APP_USER and password == APP_PASS:
            session["auth"] = True
            return redirect(request.args.get("next") or url_for("index"))
        error = "Invalid credentials"
    return render_template(
        "login.html",
        error=error,
        active=None,
        stats={},
        history=[],
        tz_name="America/New_York",
    )


@app.route("/logout")
def logout():
    session.clear()
    return redirect(url_for("login"))


if __name__ == "__main__":
    app.run(debug=True, port=5000)
