"""MyBurn Push — build payload, preview, and save receipt locally."""
from __future__ import annotations

import json
import os
import sys
import urllib.error
import urllib.request
from datetime import datetime, timezone
from pathlib import Path

from myburn import __version__
from myburn.server import _get_db, DB_PATH


RECEIPTS_DIR = Path.home() / ".myburn" / "receipts"


# ---------------------------------------------------------------------------
# Payload construction
# ---------------------------------------------------------------------------
def build_payload(month: str | None = None) -> dict:
    """Build the push payload for a given month (YYYY-MM). Defaults to current month."""
    if month is None:
        month = datetime.now().strftime("%Y-%m")
    elif len(month) <= 2:
        # User passed just MM — expand to current year
        month = f"{datetime.now().year}-{month.zfill(2)}"

    conn = _get_db()
    try:
        # --- daily_usage (aggregated across projects via __all__) ---
        daily_rows = conn.execute(
            "SELECT date, model_name, "
            "  SUM(input_tokens) AS input_tokens, SUM(output_tokens) AS output_tokens, "
            "  SUM(cache_creation_tokens) AS cache_creation_tokens, "
            "  SUM(cache_read_tokens) AS cache_read_tokens, SUM(cost) AS cost "
            "FROM daily_usage WHERE project = '__all__' AND substr(date, 1, 7) = ? "
            "GROUP BY date, model_name ORDER BY date",
            (month,),
        ).fetchall()

        total_cost = 0.0
        total_input = total_output = total_cache_read = total_cache_creation = 0
        models: dict[str, dict] = {}
        days: dict[str, float] = {}

        for r in daily_rows:
            cost = r["cost"] or 0.0
            inp = r["input_tokens"] or 0
            out = r["output_tokens"] or 0
            cr = r["cache_read_tokens"] or 0
            cc = r["cache_creation_tokens"] or 0

            total_cost += cost
            total_input += inp
            total_output += out
            total_cache_read += cr
            total_cache_creation += cc

            model = r["model_name"]
            if model not in models:
                models[model] = {"name": model, "tokens": 0, "cost": 0.0}
            models[model]["tokens"] += inp + out + cr + cc
            models[model]["cost"] += cost

            days[r["date"]] = days.get(r["date"], 0.0) + cost

        total_tokens = total_input + total_output + total_cache_read + total_cache_creation

        # Peak day
        peak_day = None
        if days:
            peak_date = max(days, key=days.get)
            peak_day = {"date": peak_date, "cost": round(days[peak_date], 2)}

        # --- sessions count (by last_activity month) ---
        session_count = conn.execute(
            "SELECT COUNT(*) FROM sessions WHERE substr(last_activity, 1, 7) = ?",
            (month,),
        ).fetchone()[0] or 0

        # --- native_turns metrics ---
        turn_row = conn.execute(
            "SELECT COUNT(*) AS total, "
            "  SUM(CASE WHEN permission_mode = 'bypassPermissions' THEN 1 ELSE 0 END) AS yolo, "
            "  AVG(CASE WHEN duration_ms IS NOT NULL THEN duration_ms END) AS avg_dur, "
            "  SUM(CASE WHEN has_thinking = 1 THEN 1 ELSE 0 END) AS thinking, "
            "  SUM(CASE WHEN is_error = 1 THEN 1 ELSE 0 END) AS errors "
            "FROM native_turns WHERE substr(timestamp, 1, 7) = ?",
            (month,),
        ).fetchone()

        total_turns = turn_row["total"] or 0
        yolo_turns = turn_row["yolo"] or 0
        thinking_count = turn_row["thinking"] or 0
        error_count = turn_row["errors"] or 0
        avg_dur = turn_row["avg_dur"]

        # Tool usage
        tool_rows = conn.execute(
            "SELECT tools_used FROM native_turns "
            "WHERE tool_count > 0 AND substr(timestamp, 1, 7) = ?",
            (month,),
        ).fetchall()
        tool_counts: dict[str, int] = {}
        for tr in tool_rows:
            try:
                tools = json.loads(tr["tools_used"])
                for t in tools:
                    tool_counts[t] = tool_counts.get(t, 0) + 1
            except (json.JSONDecodeError, TypeError):
                pass

        # --- streak & active days (from native_turns) ---
        active_dates_rows = conn.execute(
            "SELECT DISTINCT substr(timestamp, 1, 10) AS day "
            "FROM native_turns WHERE substr(timestamp, 1, 7) = ? ORDER BY day",
            (month,),
        ).fetchall()
        active_dates = sorted(r["day"] for r in active_dates_rows)
        active_days = len(active_dates)

        streak = _compute_streak(active_dates)

    finally:
        conn.close()

    # Sort models by cost descending, tools by count descending
    model_list = sorted(models.values(), key=lambda m: m["cost"], reverse=True)
    for m in model_list:
        m["cost"] = round(m["cost"], 2)
    day_list = [{"date": d, "cost": round(c, 2)} for d, c in sorted(days.items())]
    tool_sorted = dict(sorted(tool_counts.items(), key=lambda x: x[1], reverse=True))

    return {
        "version": "1.0",
        "cliVersion": __version__,
        "month": month,
        "generatedAt": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
        "cost": round(total_cost, 2),
        "tokens": {
            "total": total_tokens,
            "input": total_input,
            "output": total_output,
            "cacheRead": total_cache_read,
            "cacheCreation": total_cache_creation,
        },
        "sessions": session_count,
        "activeDays": active_days,
        "streak": streak,
        "peakDay": peak_day,
        "models": model_list,
        "days": day_list,
        "turns": {
            "total": total_turns,
            "yolo": yolo_turns,
            "yoloPct": round(yolo_turns / total_turns * 100, 1) if total_turns else 0,
            "thinkingPct": round(thinking_count / total_turns * 100, 1) if total_turns else 0,
            "errorPct": round(error_count / total_turns * 100, 1) if total_turns else 0,
            "avgDurationMs": round(avg_dur) if avg_dur else None,
        },
        "tools": tool_sorted,
    }


def _compute_streak(dates: list[str]) -> int:
    """Compute longest consecutive-day streak from a sorted list of YYYY-MM-DD strings."""
    if not dates:
        return 0
    from datetime import date as dt_date

    parsed = [dt_date.fromisoformat(d) for d in dates]
    best = current = 1
    for i in range(1, len(parsed)):
        if (parsed[i] - parsed[i - 1]).days == 1:
            current += 1
            best = max(best, current)
        else:
            current = 1
    return best


# ---------------------------------------------------------------------------
# Dry-run formatting
# ---------------------------------------------------------------------------
def _fmt_tokens(n: int) -> str:
    if n >= 1_000_000_000:
        return f"{n / 1_000_000_000:.1f}B"
    if n >= 1_000_000:
        return f"{n / 1_000_000:.1f}M"
    if n >= 1_000:
        return f"{n / 1_000:.1f}K"
    return str(n)


def _month_name(month_str: str) -> str:
    """Convert YYYY-MM to 'February 2026'."""
    from datetime import datetime as dt
    d = dt.strptime(month_str, "%Y-%m")
    return d.strftime("%B %Y")


def format_dry_run(payload: dict) -> str:
    """Format the payload as a pretty terminal preview."""
    lines = []
    lines.append(f"\n\U0001f525 MyBurn Push \u2014 {_month_name(payload['month'])}\n")

    cost = payload["cost"]
    tokens = payload["tokens"]["total"]
    sessions = payload["sessions"]
    active = payload["activeDays"]
    streak = payload["streak"]
    turns = payload["turns"]

    # Figure out days-in-month for active/total display
    from calendar import monthrange
    y, m = map(int, payload["month"].split("-"))
    _, days_in_month = monthrange(y, m)

    lines.append(f"  {'Burn':<14} ${cost:,.2f}")
    lines.append(f"  {'Tokens':<14} {_fmt_tokens(tokens)}")
    lines.append(f"  {'Sessions':<14} {sessions}")
    lines.append(f"  {'Active days':<14} {active}/{days_in_month}")
    lines.append(f"  {'Best streak':<14} {streak} days")

    if payload["peakDay"]:
        pk = payload["peakDay"]
        from datetime import datetime as dt
        pk_date = dt.strptime(pk["date"], "%Y-%m-%d").strftime("%b %d")
        lines.append(f"  {'Peak day':<14} {pk_date} (${pk['cost']:,.2f})")

    if turns["total"]:
        lines.append(f"  {'Turns':<14} {turns['total']:,}")
        lines.append(f"  {'YOLO':<14} {turns['yoloPct']}% ({turns['yolo']:,} turns)")

    lines.append("")

    # Model breakdown
    if payload["models"]:
        lines.append("  Models")
        total_cost = payload["cost"] or 1
        for mdl in payload["models"][:5]:
            pct = mdl["cost"] / total_cost * 100 if total_cost else 0
            # Shorten model name
            name = mdl["name"].replace("claude-", "").split("-2025")[0].split("-2026")[0]
            lines.append(f"    {name:<22} {pct:>5.1f}%  ${mdl['cost']:>8,.2f}")

    lines.append("")
    lines.append("  " + "\u2500" * 38)
    lines.append("  \u2713 Only aggregated numbers")
    lines.append("  \u2717 No code, prompts, or file paths")
    lines.append("")

    return "\n".join(lines)


# ---------------------------------------------------------------------------
# Receipt management
# ---------------------------------------------------------------------------
def mark_receipt_synced(month: str) -> None:
    """Add _synced + _synced_at to an existing receipt file."""
    path = RECEIPTS_DIR / f"{month}.json"
    if not path.exists():
        return
    data = json.loads(path.read_text(encoding="utf-8"))
    data["_synced"] = True
    data["_synced_at"] = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
    path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")


def list_receipts() -> list[dict]:
    """Return receipt summaries [{month, cost, synced, syncedAt, generatedAt}]."""
    if not RECEIPTS_DIR.exists():
        return []
    receipts = []
    for f in sorted(RECEIPTS_DIR.glob("*.json"), reverse=True):
        if ".v" in f.stem:
            continue  # skip versioned backups (FIX-18)
        try:
            data = json.loads(f.read_text(encoding="utf-8"))
            receipts.append({
                "month": data.get("month", f.stem),
                "cost": data.get("cost", 0),
                "synced": data.get("_synced", False),
                "syncedAt": data.get("_synced_at"),
                "generatedAt": data.get("generatedAt"),
            })
        except (json.JSONDecodeError, OSError):
            continue
    return receipts


# ---------------------------------------------------------------------------
# Cloud push
# ---------------------------------------------------------------------------
def _cloud_push(payload: dict, token: str) -> dict:
    """POST payload to myburn.cc/api/push. Returns response dict."""
    from myburn.auth import MYBURN_API
    api_url = os.environ.get("MYBURN_API", MYBURN_API)

    body = json.dumps(payload).encode()
    req = urllib.request.Request(
        f"{api_url}/api/push",
        data=body,
        headers={
            "Content-Type": "application/json",
            "Authorization": f"Bearer {token}",
            "User-Agent": "MyBurn-CLI/0.1",
        },
        method="POST",
    )
    with urllib.request.urlopen(req, timeout=30) as resp:
        return json.loads(resp.read())


def save_receipt(payload: dict) -> Path:
    """Save the payload as a local receipt with content hash. Returns the file path."""
    from myburn.trust import compute_payload_hash

    RECEIPTS_DIR.mkdir(parents=True, exist_ok=True)
    receipt = dict(payload)
    receipt["_hash"] = compute_payload_hash(payload)
    path = RECEIPTS_DIR / f"{payload['month']}.json"

    # Backup existing receipt before overwrite (FIX-18)
    if path.exists():
        n = 1
        while (RECEIPTS_DIR / f"{payload['month']}.v{n}.json").exists():
            n += 1
        path.rename(RECEIPTS_DIR / f"{payload['month']}.v{n}.json")

    path.write_text(json.dumps(receipt, indent=2) + "\n", encoding="utf-8")
    return path


def _get_prev_receipt_hash(month: str) -> str | None:
    """Get the hash from the most recent receipt (by filename), for chain linking."""
    if not RECEIPTS_DIR.exists():
        return None
    receipts = sorted(RECEIPTS_DIR.glob("*.json"), reverse=True)
    for f in receipts:
        if ".v" in f.stem:
            continue  # skip versioned backups (FIX-18)
        if f.stem == month:
            continue  # skip same month (we're about to overwrite it)
        try:
            data = json.loads(f.read_text(encoding="utf-8"))
            h = data.get("_hash")
            if h:
                return h
        except (json.JSONDecodeError, OSError):
            continue
    return None


def is_first_push() -> bool:
    """Check if this is the user's first push ever (no receipts exist)."""
    if not RECEIPTS_DIR.exists():
        return True
    return not any(RECEIPTS_DIR.glob("*.json"))


# ---------------------------------------------------------------------------
# Verify — compare local receipts vs server
# ---------------------------------------------------------------------------
def _load_full_receipts() -> dict[str, dict]:
    """Load all local receipts as {month: receipt_data}, skipping versioned backups."""
    if not RECEIPTS_DIR.exists():
        return {}
    result = {}
    for f in RECEIPTS_DIR.glob("*.json"):
        if ".v" in f.stem:
            continue
        try:
            data = json.loads(f.read_text(encoding="utf-8"))
            month = data.get("month", f.stem)
            result[month] = data
        except (json.JSONDecodeError, OSError):
            continue
    return result


def _verify_local_chain(receipts: dict[str, dict]) -> list[str]:
    """Re-compute hashes of local receipts and verify _hash fields. Returns list of issues."""
    from myburn.trust import compute_payload_hash
    issues = []
    for month in sorted(receipts):
        r = receipts[month]
        stored_hash = r.get("_hash")
        if not stored_hash:
            issues.append(f"  \u2717 {month}: no _hash in receipt")
            continue
        computed = compute_payload_hash(r)
        if computed != stored_hash:
            issues.append(f"  \u2717 {month}: hash mismatch (stored={stored_hash[:20]}... computed={computed[:20]}...)")
        else:
            issues.append(f"  \u2713 {month}: hash OK")
    return issues


def run_verify():
    """Compare local receipts with server profile data."""
    from myburn.auth import get_username, MYBURN_API

    username = get_username()
    if not username:
        print("\n  Not logged in. Run 'myburn login' first.\n")
        sys.exit(1)

    # Load local receipts
    local = _load_full_receipts()
    if not local:
        print("\n  No local receipts found. Run 'myburn push' first.\n")
        sys.exit(1)

    # Fetch server profile
    api_url = os.environ.get("MYBURN_API", MYBURN_API)
    try:
        req = urllib.request.Request(f"{api_url}/api/u/{username}", headers={"User-Agent": "MyBurn-CLI/0.1"})
        with urllib.request.urlopen(req, timeout=30) as resp:
            profile = json.loads(resp.read())
    except urllib.error.HTTPError as e:
        if e.code == 404:
            print(f"\n  Profile @{username} not found on server. Push first.\n")
            sys.exit(1)
        print(f"\n  Server error: {e.code}\n")
        sys.exit(1)
    except Exception as e:
        print(f"\n  Could not reach server: {e}\n")
        sys.exit(1)

    # Build server lookup: {month: {cost, tokens, sessions, payload_hash}}
    server = {}
    for m in profile.get("months", []):
        server[m["month"]] = m

    all_months = sorted(set(list(local.keys()) + list(server.keys())))

    print(f"\n  \U0001f50d MyBurn Verify \u2014 @{username}\n")

    matches = mismatches = one_sided = 0
    for month in all_months:
        loc = local.get(month)
        srv = server.get(month)

        if loc and not srv:
            print(f"  \u25cb {month}: local only (not pushed to server)")
            one_sided += 1
            continue
        if srv and not loc:
            print(f"  \u25cb {month}: server only (no local receipt)")
            one_sided += 1
            continue

        # Both exist — compare
        issues = []

        # Hash comparison
        local_hash = loc.get("_hash", "")
        server_hash = srv.get("payload_hash", "")
        if local_hash and server_hash and local_hash != server_hash:
            issues.append("hash mismatch")

        # Cost comparison (tolerance $0.01)
        local_cost = loc.get("cost", 0)
        server_cost = srv.get("cost", 0)
        if abs(local_cost - server_cost) > 0.01:
            issues.append(f"cost ${local_cost:.2f} vs ${server_cost:.2f}")

        # Tokens
        local_tokens = loc.get("tokens", {}).get("total", 0)
        server_tokens = srv.get("total_tokens", 0)
        if local_tokens != server_tokens:
            issues.append(f"tokens {local_tokens} vs {server_tokens}")

        # Sessions
        local_sessions = loc.get("sessions", 0)
        server_sessions = srv.get("sessions", 0)
        if local_sessions != server_sessions:
            issues.append(f"sessions {local_sessions} vs {server_sessions}")

        if issues:
            print(f"  \u2717 {month}: {', '.join(issues)}")
            mismatches += 1
        else:
            print(f"  \u2713 {month}: match")
            matches += 1

    # Local chain integrity
    print(f"\n  Chain integrity:")
    chain_issues = _verify_local_chain(local)
    for line in chain_issues:
        print(line)

    # Summary
    print(f"\n  Summary: {matches} match, {mismatches} mismatch, {one_sided} one-sided\n")


# ---------------------------------------------------------------------------
# Auto-push
# ---------------------------------------------------------------------------
_AUTO_INTERVALS = {
    "daily": 86400,
    "weekly": 604800,
    "monthly": 2592000,  # ~30 days
}


def check_auto_push() -> bool:
    """Check if auto-push is due, and if so, perform the push silently. Returns True if pushed."""
    import logging
    log = logging.getLogger("myburn.push")

    from myburn.auth import load_config, save_config, get_token

    cfg = load_config()
    mode = cfg.get("auto_push", "off")
    if mode == "off" or mode not in _AUTO_INTERVALS:
        return False

    token = get_token()
    if not token:
        return False

    # Check timing
    last_str = cfg.get("last_auto_push_at")
    now = datetime.now(timezone.utc)
    if last_str:
        try:
            last = datetime.fromisoformat(last_str.replace("Z", "+00:00"))
            elapsed = (now - last).total_seconds()
            if elapsed < _AUTO_INTERVALS[mode]:
                return False
        except (ValueError, TypeError):
            pass  # bad timestamp — push anyway

    # Build and push current month
    try:
        month = now.strftime("%Y-%m")
        if not DB_PATH.exists():
            return False

        payload = build_payload(month)
        if payload["cost"] == 0 and payload["turns"]["total"] == 0:
            return False

        save_receipt(payload)

        # Cloud push
        payload["_prevHash"] = _get_prev_receipt_hash(month) or "genesis"
        _cloud_push(payload, token)
        mark_receipt_synced(month)

        # Update last push timestamp
        cfg["last_auto_push_at"] = now.strftime("%Y-%m-%dT%H:%M:%SZ")
        save_config(cfg)

        log.info(f"Auto-push completed for {month}")
        return True
    except Exception as e:
        log.warning(f"Auto-push failed: {e}")
        return False


# ---------------------------------------------------------------------------
# CLI entry point
# ---------------------------------------------------------------------------
def run_push(dry_run: bool = False, month: str | None = None, yes: bool = False):
    """Execute the push command."""
    if not DB_PATH.exists():
        print("\n  No data found. Run 'myburn' first to sync your stats.\n")
        sys.exit(1)

    payload = build_payload(month)

    if payload["cost"] == 0 and payload["turns"]["total"] == 0:
        target = _month_name(payload["month"])
        print(f"\n  No data for {target}. Nothing to push.\n")
        sys.exit(0)

    # Always show the preview
    print(format_dry_run(payload))

    if dry_run:
        print("  (dry run \u2014 no receipt saved)\n")
        return

    # First push ever → force confirmation regardless of --yes
    first = is_first_push()
    need_confirm = not yes or first

    if need_confirm:
        if first:
            print("  This is your first push! Review the payload above.")
        try:
            answer = input("  Save receipt? [Y/n] ").strip().lower()
        except (EOFError, KeyboardInterrupt):
            print("\n  Cancelled.\n")
            sys.exit(0)
        if answer and answer not in ("y", "yes"):
            print("  Cancelled.\n")
            return

    path = save_receipt(payload)
    print(f"  Receipt saved to {path}")

    # Attempt cloud push if logged in
    from myburn.auth import get_token, get_username
    token = get_token()
    if token:
        try:
            # Add hash chain link
            payload["_prevHash"] = _get_prev_receipt_hash(payload["month"]) or "genesis"
            result = _cloud_push(payload, token)
            profile_url = result.get("profile_url", "myburn.cc")
            trust_score = result.get("trust_score")
            trust_str = f" (trust: {trust_score:.0%})" if trust_score is not None else ""
            print(f"  Published!{trust_str} View at {profile_url}\n")
        except urllib.error.HTTPError as e:
            detail = ""
            try:
                detail = json.loads(e.read()).get("detail", "")
            except Exception:
                pass
            print(f"  Cloud push failed: {e.code} {detail}")
            print(f"  Receipt saved locally. Try again with 'myburn push'.\n")
        except Exception as e:
            print(f"  Cloud push failed: {e}")
            print(f"  Receipt saved locally. Try again with 'myburn push'.\n")
    else:
        print("  Tip: Run 'myburn login' to publish to myburn.cc\n")
