# retoor <retoor@molodetz.nl>

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

from devplacepy.config import BASE_DIR, ZIPS_DIR, ZIP_STAGING_DIR
from devplacepy import project_files
from devplacepy.attachments import _directory_for
from devplacepy.services.jobs.base import JobService
from devplacepy.utils import generate_uid, slugify

logger = logging.getLogger(__name__)

WORKER_MODULE = "devplacepy.services.jobs.zip_worker"


class ZipService(JobService):
    kind = "zip"
    title = "Zip"
    description = (
        "Builds downloadable zip archives in a subprocess off the request path, tracks each "
        "job with full statistics, and deletes archives once they go unused for the retention "
        "window."
    )

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

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

        source = job["payload"].get("source", {})
        uid = job["uid"]
        staging = ZIP_STAGING_DIR / uid
        project_uid = source.get("project_uid", "")
        fail_meta = {"project_uid": project_uid}
        try:
            item_count = await asyncio.to_thread(self._materialize, source, staging)
            tmp_dir = ZIPS_DIR / _directory_for(uid)
            await asyncio.to_thread(tmp_dir.mkdir, parents=True, exist_ok=True)
            tmp_zip = tmp_dir / f"{generate_uid()}.zip"
            stats = await self._run_worker(staging, tmp_zip)
            final_name = self._final_name(job.get("preferred_name", ""), stats["crc32"])
            final_path = tmp_dir / final_name
            await asyncio.to_thread(tmp_zip.replace, final_path)
        except Exception:
            audit.record_system(
                "job.zip.failed",
                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,
                result="failure",
                target_type="project",
                target_uid=project_uid,
                metadata=fail_meta,
                summary=f"zip job for project {project_uid} failed",
                links=[audit.project(project_uid), audit.job(uid)],
            )
            raise
        finally:
            await asyncio.to_thread(shutil.rmtree, staging, ignore_errors=True)

        audit.record_system(
            "job.zip.complete",
            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,
            target_type="project",
            target_uid=project_uid,
            metadata={
                "bytes_out": stats["bytes_out"],
                "crc32": f"{stats['crc32']:08x}",
                "item_count": item_count,
            },
            summary=f"zip job for project {project_uid} completed",
            links=[audit.project(project_uid), audit.job(uid)],
        )
        return {
            "download_url": f"/zips/{uid}/download",
            "local_path": str(final_path),
            "final_name": final_name,
            "crc32": f"{stats['crc32']:08x}",
            "file_count": stats["file_count"],
            "dir_count": stats["dir_count"],
            "bytes_in": stats["bytes_in"],
            "bytes_out": stats["bytes_out"],
            "item_count": item_count,
        }

    def _materialize(self, source: dict, staging: Path) -> int:
        if source.get("type") == "project_tree":
            return project_files.export_to_dir(
                source["project_uid"], source.get("path", ""), staging
            )
        raise ValueError(f"unsupported zip source: {source.get('type')}")

    async def _run_worker(self, source_dir: Path, output_path: Path) -> dict:
        proc = await asyncio.create_subprocess_exec(
            sys.executable,
            "-m",
            WORKER_MODULE,
            str(source_dir),
            str(output_path),
            cwd=str(BASE_DIR),
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        out, err = await proc.communicate()
        if proc.returncode != 0:
            raise RuntimeError(
                f"zip worker exited {proc.returncode}: {err.decode('utf-8', 'replace')[:500]}"
            )
        return json.loads(out.decode("utf-8"))

    def _final_name(self, preferred_name: str, crc32: int) -> str:
        name = preferred_name or "download"
        if name.lower().endswith(".zip"):
            name = name[:-4]
        slug = slugify(name) or "download"
        return f"{crc32:08x}.{slug}.zip"

    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)
        shutil.rmtree(ZIP_STAGING_DIR / job["uid"], ignore_errors=True)
