# retoor <retoor@molodetz.nl>
from __future__ import annotations

import asyncio
import json
import logging
import shutil
import sys
from pathlib import Path

from devplacepy.config import BASE_DIR, INTERNAL_MODEL, ISSLOP_RUNS_DIR, ISSLOP_WORKSPACES_DIR
from devplacepy.database import INTERNAL_GATEWAY_URL, get_int_setting, get_setting, internal_gateway_key
from devplacepy.services import pubsub
from devplacepy.services.base import ConfigField
from devplacepy.services.jobs.base import JobService
from devplacepy.services.jobs.isslop import store
from devplacepy.services.jobs.isslop.acquisition.workspace import remove_workspace, workspace_for
from devplacepy.services.jobs.isslop.config import IMAGE_MAX_COUNT, WORKER_TIMEOUT_SECONDS
from devplacepy.services.jobs.isslop.events import KIND_DONE, KIND_ERROR, WorkerEvent
from devplacepy.services.jobs.isslop.persistence import EventPersister

logger = logging.getLogger(__name__)

WORKER_MODULE = "devplacepy.services.jobs.isslop.worker"
STREAM_LIMIT = 16 * 1024 * 1024
TOPIC_PREFIX = "public.isslop."


def topic_for(uid: str) -> str:
    return f"{TOPIC_PREFIX}{uid}"


class IsslopService(JobService):
    kind = "isslop"
    title = "AI Usage Analyzer"
    description = (
        "Classifies a git repository or website as AI slop, sophisticated AI-assisted work or "
        "genuine human work: acquires the source in an isolated subprocess, runs a multi-signal "
        "static analysis plus AI and vision review passes through the internal gateway, streams "
        "every step live over pub/sub and persists a shareable report with an authenticity badge."
    )

    def __init__(self):
        super().__init__(name="isslop", interval_seconds=2)
        for field in self.config_fields:
            if field.key == self.timeout_key:
                field.default = WORKER_TIMEOUT_SECONDS
        self.config_fields += [
            ConfigField(
                "isslop_allow_private",
                "Allow private hosts",
                type="bool",
                default="0",
                help="Permit analyzing private, loopback and link-local hosts. Keep off in production (SSRF).",
                group="Analysis",
            ),
            ConfigField(
                "isslop_ai_review",
                "AI review pass",
                type="bool",
                default="1",
                help="Audit representative files with the AI gateway on top of the static engine.",
                group="Analysis",
            ),
            ConfigField(
                "isslop_image_review",
                "Image review pass",
                type="bool",
                default="1",
                help="Grade images for AI generation with the vision model.",
                group="Analysis",
            ),
            ConfigField(
                "isslop_template_detection",
                "Template provenance detection",
                type="bool",
                default="1",
                help="Detect starter-template/boilerplate provenance and count shipped scaffold defaults against the authenticity grade.",
                group="Analysis",
            ),
            ConfigField(
                "isslop_image_max",
                "Max images per analysis",
                type="int",
                default=IMAGE_MAX_COUNT,
                minimum=1,
                maximum=100,
                help="Deterministically sampled cap on images sent to the vision model.",
                group="Analysis",
            ),
            ConfigField(
                "isslop_ai_model",
                "AI model",
                type="str",
                default="",
                help="Model sent to the internal AI gateway for the file review and image review passes. Blank uses the gateway default model.",
                group="Analysis",
            ),
        ]

    def run_dir(self, uid: str) -> Path:
        return ISSLOP_RUNS_DIR / uid

    def _worker_payload(self, job: dict) -> dict:
        payload = dict(job.get("payload", {}))
        payload["llm_endpoint"] = INTERNAL_GATEWAY_URL
        payload["llm_model"] = get_setting("isslop_ai_model", "") or INTERNAL_MODEL
        payload["api_key"] = internal_gateway_key()
        payload["allow_private"] = get_setting("isslop_allow_private", "0") == "1"
        payload["ai_review"] = get_setting("isslop_ai_review", "1") == "1"
        payload["image_review"] = get_setting("isslop_image_review", "1") == "1"
        payload["image_max"] = max(1, get_int_setting("isslop_image_max", IMAGE_MAX_COUNT))
        payload["media_dir"] = str(store.media_dir_for(job["uid"]))
        payload["template_detection"] = get_setting("isslop_template_detection", "1") == "1"
        return payload

    async def _relay(self, persister: EventPersister, event: WorkerEvent) -> None:
        enriched = await asyncio.to_thread(persister.apply, event)
        await pubsub.publish(topic_for(persister.analysis_uid), enriched)

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

        uid = job["uid"]
        source_url = str(job.get("payload", {}).get("url", ""))
        actor_kind = "user" if job.get("owner_kind") == "user" else (job.get("owner_kind") or "system")
        actor_uid = job.get("owner_id") if job.get("owner_kind") == "user" else None
        persister = EventPersister(uid)
        store.reset_evidence(uid)
        store.update_analysis(uid, status="running")
        await pubsub.publish(
            topic_for(uid),
            {
                "analysis": uid,
                "seq": 0,
                "kind": "status",
                "message": "running",
                "data": {},
                "created_at": store.utc_now(),
            },
        )

        run_dir = self.run_dir(uid)
        await asyncio.to_thread(run_dir.mkdir, parents=True, exist_ok=True)
        payload_path = run_dir / "payload.json"
        await asyncio.to_thread(
            payload_path.write_text, json.dumps(self._worker_payload(job)), encoding="utf-8"
        )
        try:
            workspace = workspace_for(ISSLOP_WORKSPACES_DIR, source_url, uid)
        except ValueError as error:
            await self._relay(persister, WorkerEvent(KIND_ERROR, f"Workspace rejected: {error}", {}))
            await asyncio.to_thread(shutil.rmtree, run_dir, ignore_errors=True)
            raise RuntimeError(str(error))

        try:
            final = await self._run_worker(uid, persister, payload_path, workspace)
        finally:
            await asyncio.to_thread(remove_workspace, workspace)
            await asyncio.to_thread(shutil.rmtree, run_dir, ignore_errors=True)

        if final.kind == KIND_ERROR:
            audit.record_system(
                "isslop.run.failed",
                actor_kind=actor_kind,
                actor_uid=actor_uid,
                result="failure",
                summary=f"AI usage analysis of {source_url} failed",
                metadata={"target": source_url, "error": final.message[:200]},
                links=[audit.job(uid)],
            )
            raise RuntimeError(final.message)

        audit.record_system(
            "isslop.run.complete",
            actor_kind=actor_kind,
            actor_uid=actor_uid,
            summary=f"AI usage analysis of {source_url} graded {final.data.get('grade')}",
            metadata={
                "target": source_url,
                "grade": final.data.get("grade"),
                "category": final.data.get("category"),
                "human_percent": final.data.get("human_percent"),
                "files_analyzed": final.data.get("files_analyzed"),
            },
            links=[audit.job(uid)],
        )
        return {
            "target": source_url,
            "grade": final.data.get("grade"),
            "category": final.data.get("category"),
            "human_percent": final.data.get("human_percent"),
            "ai_percent": final.data.get("ai_percent"),
            "report_url": f"/tools/isslop/{uid}/report",
            "bytes_in": 0,
            "bytes_out": 0,
            "item_count": int(final.data.get("files_analyzed") or 0),
        }

    async def _run_worker(
        self, uid: str, persister: EventPersister, payload_path: Path, workspace: Path
    ) -> WorkerEvent:
        proc = await asyncio.create_subprocess_exec(
            sys.executable,
            "-m",
            WORKER_MODULE,
            str(payload_path),
            str(workspace),
            cwd=str(BASE_DIR),
            stdin=asyncio.subprocess.DEVNULL,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
            limit=STREAM_LIMIT,
        )
        final: WorkerEvent | None = None
        stderr_task = asyncio.create_task(proc.stderr.read())
        try:
            while True:
                line = await proc.stdout.readline()
                if not line:
                    break
                event = WorkerEvent.parse(line.decode("utf-8", "replace"))
                if event is None:
                    continue
                await self._relay(persister, event)
                if event.kind in (KIND_DONE, KIND_ERROR):
                    final = event
            await stderr_task
            await proc.wait()
        finally:
            if proc.returncode is None:
                try:
                    proc.kill()
                except ProcessLookupError:
                    pass
                await proc.wait()
            if not stderr_task.done():
                stderr_task.cancel()
        if final is None:
            final = WorkerEvent(
                KIND_ERROR, f"Analysis worker exited unexpectedly with code {proc.returncode}", {}
            )
            await self._relay(persister, final)
        return final

    def cleanup(self, job: dict) -> None:
        shutil.rmtree(self.run_dir(job["uid"]), ignore_errors=True)
