# 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, SEO_REPORTS_DIR
from devplacepy.services.jobs.base import JobService
from .progress import hub

logger = logging.getLogger(__name__)

WORKER_MODULE = "devplacepy.services.jobs.seo.worker"
STREAM_LIMIT = 16 * 1024 * 1024


class SeoService(JobService):
    kind = "seo"
    title = "SEO Diagnostics"
    description = (
        "Audits any URL or sitemap with a broad battery of technical, on-page, structured-data, "
        "performance, accessibility and AI-readiness checks using a headless browser, streaming "
        "live progress over a websocket and storing a categorised report."
    )

    def __init__(self):
        super().__init__(name="seo", interval_seconds=2)

    def report_dir(self, uid: str) -> Path:
        return SEO_REPORTS_DIR / uid

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

        uid = job["uid"]
        payload = job.get("payload", {})
        target = payload.get("url", "")
        output_dir = self.report_dir(uid)
        await asyncio.to_thread(output_dir.mkdir, parents=True, exist_ok=True)
        payload_path = output_dir / "payload.json"
        await asyncio.to_thread(payload_path.write_text, json.dumps(payload), encoding="utf-8")

        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

        try:
            summary = await self._run_worker(uid, payload_path, output_dir)
        except Exception as exc:
            hub.publish(uid, {"type": "failed", "message": str(exc)[:300]})
            audit.record_system(
                "seo.run.failed",
                actor_kind=actor_kind,
                actor_uid=actor_uid,
                result="failure",
                summary=f"SEO diagnostics for {target} failed",
                metadata={"target": target, "error": str(exc)[:200]},
                links=[audit.job(uid)],
            )
            await asyncio.to_thread(shutil.rmtree, output_dir, ignore_errors=True)
            raise

        report = await self._load_report(output_dir)
        hub.publish(
            uid,
            {
                "type": "done",
                "score": summary.get("score"),
                "grade": summary.get("grade"),
                "page_count": summary.get("page_count"),
                "report_url": f"/tools/seo/{uid}/report",
                "report": report,
            },
        )
        hub.clear(uid)
        audit.record_system(
            "seo.run.complete",
            actor_kind=actor_kind,
            actor_uid=actor_uid,
            summary=f"SEO diagnostics for {target} scored {summary.get('score')}",
            metadata={
                "target": target,
                "score": summary.get("score"),
                "grade": summary.get("grade"),
                "page_count": summary.get("page_count"),
            },
            links=[audit.job(uid)],
        )
        return {
            "target": target,
            "mode": payload.get("mode", "url"),
            "score": summary.get("score", 0),
            "grade": summary.get("grade", ""),
            "page_count": summary.get("page_count", 0),
            "counts": summary.get("counts", {}),
            "categories": summary.get("categories", {}),
            "report": report,
            "report_url": f"/tools/seo/{uid}/report",
            "bytes_in": 0,
            "bytes_out": len(json.dumps(report)) if report else 0,
            "item_count": summary.get("page_count", 0),
        }

    async def _run_worker(self, uid: str, payload_path: Path, output_dir: Path) -> dict:
        proc = await asyncio.create_subprocess_exec(
            sys.executable,
            "-m",
            WORKER_MODULE,
            str(payload_path),
            str(output_dir),
            cwd=str(BASE_DIR),
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
            limit=STREAM_LIMIT,
        )
        summary: dict = {}
        worker_error = ""
        err = ""
        stderr_task = asyncio.create_task(proc.stderr.read())
        try:
            while True:
                line = await proc.stdout.readline()
                if not line:
                    break
                try:
                    frame = json.loads(line.decode("utf-8", "replace"))
                except (ValueError, TypeError):
                    continue
                hub.publish(uid, frame)
                if frame.get("type") == "report_ready":
                    summary = frame
                elif frame.get("type") == "error":
                    worker_error = frame.get("message", "worker error")
            err = (await stderr_task).decode("utf-8", "replace")
            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 proc.returncode != 0 or not summary:
            raise RuntimeError(
                worker_error or err[:500] or f"seo worker exited {proc.returncode}"
            )
        return summary

    async def _load_report(self, output_dir: Path) -> dict:
        report_path = output_dir / "report.json"
        if not report_path.is_file():
            return {}
        try:
            text = await asyncio.to_thread(report_path.read_text, encoding="utf-8")
            return json.loads(text)
        except (ValueError, OSError):
            return {}

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