"""Routes for the Help panel: the product-knowledge feed, and the diagnostic bundle for bug reports.

The bundle is assembled LOCALLY and only revealed in the file manager; nothing uploads anywhere by
itself. Contents are deliberately allowlisted (identity, versions, feature booleans, provider KINDS,
counts, log tail) so no secret or API key can ever ride along.
"""

import base64
import json
import os
import platform
import re
import sys
import time
from contextlib import asynccontextmanager
from typing import AsyncIterator, List

from pydantic import BaseModel, ConfigDict, Field
from typeguard import typechecked

from backend.apps.help.knowledge import HelpKnowledgeResponse, build_knowledge_response
from backend.config.Apps import SubApp
from backend.config.paths import DATA_ROOT, SESSIONS_DIR


@asynccontextmanager
async def help_lifespan() -> AsyncIterator[None]:
    yield


help_app = SubApp("help", help_lifespan)

DIAG_DIR = os.path.join(DATA_ROOT, "diagnostics")
LOG_TAIL_LINES = 200
MAX_ATTACHMENTS = 6
MAX_ATTACHMENT_BYTES = 8 * 1024 * 1024
# Key-shaped strings never belong in a shareable report, even from free-text log lines.
P_SECRET_RE = re.compile(r"(sk-[A-Za-z0-9\-]{8,}|Bearer\s+\S+|api[_-]?key[\"']?\s*[:=]\s*\S+)", re.IGNORECASE)


class BundleAttachment(BaseModel):
    model_config = ConfigDict(validate_assignment=True)

    name: str
    data_b64: str


class BundleRequest(BaseModel):
    model_config = ConfigDict(validate_assignment=True)

    kind: str = "bug"
    description: str = ""
    attachments: List[BundleAttachment] = Field(default_factory=list)


@typechecked
def p_safe_name(name: str) -> str:
    base = os.path.basename(name or "attachment")
    return re.sub(r"[^A-Za-z0-9._-]", "_", base)[:80] or "attachment"


@typechecked
def p_scrub(text: str) -> str:
    return P_SECRET_RE.sub("[redacted]", text)


@typechecked
def p_log_tail() -> str:
    """Last lines of the backend log when packaged (Electron writes backend.log next to the data
    root); dev runs log to the terminal, so a missing file just yields an honest note."""
    candidates = [
        os.path.join(os.path.dirname(DATA_ROOT), "backend.log"),
        os.path.join(DATA_ROOT, "backend.log"),
    ]
    for p in candidates:
        try:
            if os.path.isfile(p):
                with open(p, "r", errors="replace") as fh:
                    lines = fh.readlines()[-LOG_TAIL_LINES:]
                return p_scrub("".join(lines))
        except Exception:
            continue
    return "(no backend.log found; dev runs log to the terminal)"


@typechecked
def p_count_dir(path: str) -> int:
    try:
        return len(os.listdir(path))
    except Exception:
        return 0


@typechecked
def p_recent_crash_reports(limit: int = 3) -> str:
    """The newest Electron crash reports (ENG-102), metadata only: the backend-log tail inside each
    report is dropped here because this bundle already carries its own scrubbed tail."""
    try:
        crash_dir = os.path.join(os.path.dirname(DATA_ROOT), "crash-reports")
        files = sorted((f for f in os.listdir(crash_dir) if f.endswith(".json")), reverse=True)[:limit]
        if not files:
            return "(none)"
        out: List[str] = []
        for name in files:
            with open(os.path.join(crash_dir, name), "r", encoding="utf-8") as fh:
                data = json.load(fh)
            data.pop("backendLogTail", None)
            out.append(json.dumps(data, indent=2, default=str))
        return "\n".join(out)
    except Exception:
        return "(none)"


@typechecked
def p_build_report(req: BundleRequest) -> str:
    from backend.apps.settings.store import load_settings

    s = load_settings()
    provider_kinds: List[str] = []
    if getattr(s, "anthropic_api_key", None):
        provider_kinds.append("anthropic-key")
    if getattr(s, "openai_api_key", None):
        provider_kinds.append("openai-key")
    if getattr(s, "free_trial_token", None):
        provider_kinds.append("free-trial")
    facts = {
        "kind": req.kind,
        "created_at": time.strftime("%Y-%m-%d %H:%M:%S"),
        "app_version": os.environ.get("OPENSWARM_APP_VERSION", "dev"),
        "platform": f"{platform.system()} {platform.release()} ({platform.machine()})",
        "python": sys.version.split()[0],
        "packaged": os.environ.get("OPENSWARM_PACKAGED") == "1",
        "installation_id": getattr(s, "installation_id", None),
        "user_email": getattr(s, "user_email", None),
        "signin_method": getattr(s, "signin_method", None),
        "default_model": getattr(s, "default_model", None),
        "connection_mode": getattr(s, "connection_mode", None),
        "provider_kinds": provider_kinds,
        "session_count": p_count_dir(SESSIONS_DIR),
        "theme": getattr(s, "theme", None),
    }
    lines = [
        f"# OpenSwarm {('bug report' if req.kind == 'bug' else 'feature request')}",
        "",
        "## What the user reported",
        req.description.strip() or "(no description)",
        "",
        "## Environment",
        "```json",
        json.dumps(facts, indent=2, default=str),
        "```",
        "",
        "## Recent backend log",
        "```",
        p_log_tail(),
        "```",
        "",
        "## Recent crashes",
        "```json",
        p_recent_crash_reports(),
        "```",
        "",
    ]
    return "\n".join(lines)


@help_app.router.get("/knowledge")
@typechecked
async def get_help_knowledge() -> HelpKnowledgeResponse:
    return build_knowledge_response()


@help_app.router.get("/whats-new")
def whats_new() -> dict:
    """The release story for the in-app What's New card. Same words the Help agent and the GitHub
    body get, so a user never reads two different accounts of the same release."""
    from backend.apps.help.changelog import as_markdown, latest_release, release_notes
    from backend.apps.service.version import APP_VERSION
    note = release_notes(APP_VERSION) or latest_release()
    return {
        "version": note.version,
        "headline": note.headline,
        "highlights": note.highlights,
        "fixes": note.fixes,
        "markdown": as_markdown(note),
    }


@help_app.router.post("/bundle")
@typechecked
async def build_bundle(body: BundleRequest) -> dict:
    stamp = time.strftime("%Y%m%d-%H%M%S")
    folder = os.path.join(DIAG_DIR, f"report-{stamp}")
    os.makedirs(folder, exist_ok=True)
    report_path = os.path.join(folder, "diagnostic-report.md")
    with open(report_path, "w") as fh:
        fh.write(p_build_report(body))
    saved: List[str] = []
    for att in body.attachments[:MAX_ATTACHMENTS]:
        try:
            raw = base64.b64decode(att.data_b64)
            if len(raw) > MAX_ATTACHMENT_BYTES:
                continue
            dest = os.path.join(folder, p_safe_name(att.name))
            with open(dest, "wb") as fh:
                fh.write(raw)
            saved.append(os.path.basename(dest))
        except Exception:
            continue
    return {"folder": folder, "report": report_path, "attachments": saved}
