# retoor <retoor@molodetz.nl>

import logging

import httpx

from devplacepy import stealth
from devplacepy.config import INTERNAL_GATEWAY_URL
from devplacepy.services.gitea.client import STATE_OPEN
from devplacepy.services.gitea.config import GiteaConfig
from devplacepy.services.openai_gateway.usage import accumulate_usage

logger = logging.getLogger(__name__)

MAX_TOKENS = 32000
MAX_ISSUES = 50
PAGE_LIMIT = 50
BODY_MAX = 40000
PLAN_MAX = 600000
PLANNING_TIMEOUT_SECONDS = 600.0
UNGROUPED_LABEL = "General"

SYSTEM_PROMPT = (
    "You are a senior engineering lead for DevPlace, a server-rendered social network for "
    "software developers (FastAPI + Jinja2 + pure ES6 modules, SQLite via the dataset "
    "library). You are given the COMPLETE, VERBATIM text of every OPEN issue ticket from a "
    "Gitea tracker. Your job is to produce ONE single, exhaustive implementation document "
    "that an autonomous coding agent (Claude) can execute end to end in one shot, ticket by "
    "ticket, with NO further questions and NO missing information.\n\n"
    "ABSOLUTE RULES:\n"
    "- Never invent tickets, numbers, facts, requirements, or constraints that are not in "
    "the input. If a detail is missing from a ticket, say so explicitly rather than guessing.\n"
    "- PRESERVE EVERY DETAIL from each ticket description. Do not summarise away, shorten, or "
    "drop any requirement, reproduction step, acceptance criterion, edge case, link, code "
    "snippet, or example. Reproduce the full original requirement text for each ticket "
    "verbatim in its 'Original ticket' block, then expand on it - never replace it.\n"
    "- The document must be self-contained and directly executable: enough detail that the "
    "agent never has to ask a clarifying question.\n"
    "- Output ONLY GitHub-flavored markdown. No preamble, no commentary outside the document.\n\n"
    "Use EXACTLY this structure:\n\n"
    "# Open Tickets Implementation Plan\n"
    "One paragraph stating how many tickets are covered, the phasing strategy, and the "
    "ordering rationale (dependencies and high-impact/blocking work first).\n\n"
    "## Execution Order\n"
    "A flat numbered list of every ticket number in the exact recommended execution order, "
    "each line as '#N - <title>'.\n\n"
    "Then group the work into PHASES, one '## Phase K: <name>' heading per phase, ordered so "
    "blocking and high-impact work comes first. Start each phase with a one-paragraph goal and "
    "any prerequisites (which earlier phases or tickets must land first).\n\n"
    "Inside each phase, for EVERY ticket it contains, emit a '### #N <title>' subsection with "
    "ALL of the following labelled blocks, in this order:\n"
    "- **Original ticket**: the complete, unedited ticket description text, quoted verbatim "
    "(use a blockquote). Include labels.\n"
    "- **Goal**: what 'done' means for this ticket in one or two sentences.\n"
    "- **Dependencies**: other ticket numbers or phases this depends on, or 'None'.\n"
    "- **Affected areas / files**: the concrete modules, routers, templates, JS, CSS, services, "
    "schemas, and docs likely to change (infer from the DevPlace architecture; name real-looking "
    "paths where the ticket implies them, and mark anything uncertain as 'to confirm').\n"
    "- **Implementation steps**: a numbered, ordered, concrete checklist the agent follows to "
    "implement the ticket across the full stack (data layer, server, view, agent/Devii, docs, "
    "SEO as applicable). Be specific and actionable, not generic.\n"
    "- **Acceptance criteria**: a checklist of verifiable conditions that prove the ticket is "
    "complete, derived strictly from the ticket plus the implementation steps.\n"
    "- **Risks / open questions**: anything ambiguous in the ticket the agent should watch for; "
    "write 'None' if the ticket is fully specified.\n\n"
    "Be as long and detailed as necessary. Do not truncate. Completeness beats brevity."
)


def _body(text: str) -> str:
    text = (text or "").strip().replace("\r\n", "\n")
    if len(text) > BODY_MAX:
        return text[:BODY_MAX].rstrip() + "\n\n[description truncated at safety limit]"
    return text


def _labels(issue: dict) -> list[str]:
    labels = issue.get("labels") or []
    names: list[str] = []
    for label in labels:
        if isinstance(label, dict):
            name = str(label.get("name", "")).strip()
        else:
            name = str(label).strip()
        if name:
            names.append(name)
    return names


def _group_key(issue: dict) -> str:
    labels = _labels(issue)
    return labels[0] if labels else UNGROUPED_LABEL


async def collect_open_issues(client, limit: int = MAX_ISSUES) -> list[dict]:
    collected: list[dict] = []
    page = 1
    while len(collected) < limit:
        issues, total = await client.list_issues(
            state=STATE_OPEN, page=page, limit=PAGE_LIMIT
        )
        if not issues:
            break
        collected.extend(issues)
        if len(issues) < PAGE_LIMIT or len(collected) >= total:
            break
        page += 1
    return collected[:limit]


def verbatim_tickets(issues: list[dict]) -> str:
    lines: list[str] = ["# Appendix: Source Tickets (verbatim)", ""]
    lines.append(
        "The complete, unedited description of every ticket covered by this plan, reproduced "
        "directly from the tracker so the document is self-contained."
    )
    lines.append("")
    for issue in issues:
        number = int(issue.get("number", 0))
        title = str(issue.get("title", "")).strip() or "Untitled"
        labels = ", ".join(_labels(issue)) or "none"
        url = str(issue.get("html_url", "")).strip()
        body = _body(issue.get("body", "")) or "No description provided."
        lines.append(f"## #{number} {title}")
        lines.append("")
        lines.append(f"**Labels**: {labels}")
        if url:
            lines.append("")
            lines.append(f"**Source**: {url}")
        lines.append("")
        lines.append("**Original ticket**:")
        lines.append("")
        for body_line in body.split("\n"):
            lines.append(f"> {body_line}")
        lines.append("")
    return "\n".join(lines)


def _fallback(issues: list[dict]) -> str:
    groups: dict[str, list[dict]] = {}
    for issue in issues:
        groups.setdefault(_group_key(issue), []).append(issue)

    ordered_group_names = sorted(
        groups,
        key=lambda name: (name == UNGROUPED_LABEL, name.lower()),
    )

    lines: list[str] = ["# Open Tickets Implementation Plan", ""]
    lines.append(
        f"Deterministic plan covering {len(issues)} open ticket"
        f"{'' if len(issues) == 1 else 's'}, grouped by primary label and ordered by "
        "ticket number within each group. The full original description of every ticket is "
        "reproduced verbatim below."
    )
    lines.append("")

    lines.append("## Execution Order")
    for position, issue in enumerate(issues, start=1):
        number = int(issue.get("number", 0))
        title = str(issue.get("title", "")).strip() or "Untitled"
        lines.append(f"{position}. #{number} - {title}")
    lines.append("")

    for index, name in enumerate(ordered_group_names, start=1):
        members = sorted(groups[name], key=lambda i: int(i.get("number", 0)))
        lines.append(f"## Phase {index}: {name}")
        lines.append("")
        for issue in members:
            number = int(issue.get("number", 0))
            title = str(issue.get("title", "")).strip() or "Untitled"
            labels = ", ".join(_labels(issue)) or "none"
            body = _body(issue.get("body", "")) or "No description provided."
            lines.append(f"### #{number} {title}")
            lines.append("")
            lines.append(f"**Labels**: {labels}")
            lines.append("")
            lines.append("**Original ticket**:")
            lines.append("")
            for body_line in body.split("\n"):
                lines.append(f"> {body_line}")
            lines.append("")
    lines.append("")

    return "\n".join(lines)[:PLAN_MAX]


def _user_message(issues: list[dict]) -> str:
    parts: list[str] = [
        f"There are {len(issues)} open tickets. Their complete, verbatim descriptions "
        "follow. Produce the full implementation document covering every one of them.",
        "",
    ]
    for issue in issues:
        number = int(issue.get("number", 0))
        title = str(issue.get("title", "")).strip() or "Untitled"
        labels = ", ".join(_labels(issue)) or "none"
        body = _body(issue.get("body", ""))
        parts.append(f"===== TICKET #{number} =====")
        parts.append(f"title: {title}")
        parts.append(f"labels: {labels}")
        parts.append("description (verbatim):")
        parts.append(body or "none")
        parts.append("")
    return "\n".join(parts)


async def generate_plan(
    issues: list[dict], config: GiteaConfig, totals: dict | None = None
) -> tuple[str, bool]:
    issues = list(issues)[:MAX_ISSUES]
    if not issues:
        return (
            "# Open Tickets Implementation Plan\n\nThere are no open tickets to plan.\n",
            False,
        )
    if not config.ai_enhance:
        return _fallback(issues), False

    payload = {
        "model": config.ai_model,
        "messages": [
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": _user_message(issues)},
        ],
        "max_tokens": MAX_TOKENS,
        "temperature": 0.2,
    }
    headers = {
        "Content-Type": "application/json",
        "X-App-Reference": "devplace-gitea-planning-v-1-0-0",
    }
    if config.ai_key:
        headers["Authorization"] = f"Bearer {config.ai_key}"

    try:
        async with stealth.stealth_async_client(timeout=PLANNING_TIMEOUT_SECONDS) as client:
            response = await client.post(
                INTERNAL_GATEWAY_URL, json=payload, headers=headers
            )
            response.raise_for_status()
            accumulate_usage(totals, response)
            content = (
                response.json()
                .get("choices", [{}])[0]
                .get("message", {})
                .get("content", "")
            )
    except (httpx.HTTPError, ValueError, KeyError, IndexError) as exc:
        logger.warning("Planning generation failed, using fallback: %s", exc)
        return _fallback(issues), False

    markdown = (content or "").strip()
    if not markdown:
        logger.warning("Planning generation returned empty content, using fallback")
        return _fallback(issues), False
    logger.info("Generated AI planning for %d open tickets", len(issues))
    document = f"{markdown}\n\n---\n\n{verbatim_tickets(issues)}"
    return document[:PLAN_MAX], True
