# retoor <retoor@molodetz.nl>

import asyncio
import logging
import zlib
from pathlib import Path

from devplacepy.attachments import _directory_for
from devplacepy.config import PLANNING_REPORTS_DIR
from devplacepy.database import add_issue_usage
from devplacepy.services.gitea import runtime
from devplacepy.services.gitea.client import GiteaError
from devplacepy.services.gitea.config import gitea_config
from devplacepy.services.gitea.planning import collect_open_issues, generate_plan
from devplacepy.services.jobs.base import JobService
from devplacepy.services.openai_gateway.usage import new_usage_totals
from devplacepy.utils import slugify

logger = logging.getLogger(__name__)


class PlanningReportService(JobService):
    kind = "planning"
    title = "Ticket planning"
    description = (
        "Builds a complete, phased markdown implementation document of all open Gitea "
        "tickets off the request path using the internal AI service (with a deterministic "
        "fallback), writes the markdown artifact, and prunes it once unused for the "
        "retention window. AI usage is metered into the issue tracker statistics."
    )

    def __init__(self) -> None:
        super().__init__(name="planning", interval_seconds=2)

    async def process(self, job: dict) -> dict:
        from devplacepy.services.audit import record as audit

        uid: str = job["uid"]
        owner_kind: str = job.get("owner_kind") or "system"
        owner_id: str = job.get("owner_id") or ""
        try:
            config = gitea_config()
            if not config.is_configured:
                raise GiteaError("Gitea integration is not configured", status=503)

            numbers = self._selected_numbers(job)
            issues = await self._collect(numbers)
            usage_totals = new_usage_totals()
            markdown, ai_used = await generate_plan(issues, config, usage_totals)
            issue_count = len(issues)
            if usage_totals["calls"]:
                add_issue_usage(usage_totals)

            final_name = self._final_name(job.get("preferred_name", ""), markdown)
            target_dir = PLANNING_REPORTS_DIR / _directory_for(uid)
            await asyncio.to_thread(target_dir.mkdir, parents=True, exist_ok=True)
            final_path = target_dir / final_name
            await asyncio.to_thread(final_path.write_text, markdown, encoding="utf-8")
            bytes_out = len(markdown.encode("utf-8"))
        except Exception:
            audit.record_system(
                "issue.planning.generate",
                actor_kind="user" if owner_kind == "user" else owner_kind,
                actor_uid=owner_id if owner_kind == "user" else None,
                result="failure",
                target_type="issue",
                metadata={"job": uid},
                summary="planning report generation failed",
                links=[audit.job(uid)],
            )
            raise

        audit.record_system(
            "issue.planning.generate",
            actor_kind="user" if owner_kind == "user" else owner_kind,
            actor_uid=owner_id if owner_kind == "user" else None,
            target_type="issue",
            metadata={
                "issue_count": issue_count,
                "selected": len(numbers),
                "ai_used": ai_used,
                "bytes_out": bytes_out,
            },
            summary=f"planning report for {issue_count} open tickets generated",
            links=[audit.job(uid)],
        )
        self.log(f"Planning job {uid} covered {issue_count} open tickets")
        return {
            "download_url": f"/issues/planning/{uid}/download",
            "local_path": str(final_path),
            "final_name": final_name,
            "markdown": markdown,
            "ai_used": ai_used,
            "item_count": issue_count,
            "bytes_out": bytes_out,
        }

    def _selected_numbers(self, job: dict) -> list[int]:
        raw = job.get("payload", {}).get("numbers") or []
        numbers: list[int] = []
        for value in raw:
            try:
                number = int(value)
            except (TypeError, ValueError):
                continue
            if number not in numbers:
                numbers.append(number)
        return numbers

    async def _collect(self, numbers: list[int]) -> list[dict]:
        open_issues = await collect_open_issues(runtime.get_client())
        if not numbers:
            return open_issues
        by_number = {int(issue.get("number", 0)): issue for issue in open_issues}
        return [by_number[number] for number in numbers if number in by_number]

    def _final_name(self, preferred_name: str, markdown: str) -> str:
        name = preferred_name or "open-tickets-plan"
        if name.lower().endswith(".md"):
            name = name[:-3]
        slug = slugify(name) or "open-tickets-plan"
        crc32 = zlib.crc32(markdown.encode("utf-8")) & 0xFFFFFFFF
        return f"{crc32:08x}.{slug}.md"

    def cleanup(self, job: dict) -> None:
        result = job.get("result", {})
        local_path = result.get("local_path")
        if local_path:
            Path(local_path).unlink(missing_ok=True)
