"""BackupWatchman — file monitoring and upload plan generation."""

from __future__ import annotations

import json
import logging
import os
from collections.abc import Callable
from datetime import datetime, timezone
from pathlib import Path

from file_watchman import (
    HashPolicy,
    ScanRules,
    diff_manifests,
    dump_manifest,
    load_manifest,
    scan_manifest,
    write_manifest_atomic,
)
from file_watchman.model import Delta

logger = logging.getLogger(__name__)

DEFAULT_MULTIPART_THRESHOLD = 50 * 1024 * 1024  # 50 MB


class BackupWatchman:
    """Monitors a directory, diffs against previous state, and produces upload plans.

    Each scan cycle:
    1. Scans the watch directory into a manifest
    2. Diffs against the previous manifest
    3. Builds an upload plan (files sorted largest first)
    4. Rotates manifests (new -> prev)
    """

    def __init__(
        self,
        watch_dir: str | Path,
        output_dir: str | Path,
        *,
        rules: ScanRules | None = None,
        hash_policy: HashPolicy | None = None,
        categorize: Callable[[str], str] | None = None,
        job_id: str | None = None,
        multipart_threshold: int = DEFAULT_MULTIPART_THRESHOLD,
    ) -> None:
        self.watch_dir = str(watch_dir)
        self.output_dir = Path(output_dir)
        self.output_dir.mkdir(parents=True, exist_ok=True)
        self.rules = rules or ScanRules(includes=["**/*"], excludes=[])
        self.hash_policy = hash_policy or HashPolicy(
            required_categories=set(),
            optional_categories=set(),
            never_categories=set(),
            max_hash_bytes=2 * 1024 * 1024 * 1024,
        )
        self.categorize = categorize
        self.job_id = job_id
        self.multipart_threshold = multipart_threshold

        self._manifest_prev = self.output_dir / "manifest_prev.json"
        self._manifest_new = self.output_dir / "manifest_new.json"

    def scan(self) -> dict:
        """Run a single scan-diff-plan cycle. Returns the upload plan dict."""
        ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S")

        manifest_new = scan_manifest(
            root=self.watch_dir,
            rules=self.rules,
            hash_policy=self.hash_policy,
            categorize=self.categorize,
            job_id=self.job_id,
        )
        write_manifest_atomic(str(self._manifest_new), manifest_new)

        if self._manifest_prev.exists():
            manifest_prev = load_manifest(str(self._manifest_prev))
            delta = diff_manifests(manifest_prev, manifest_new)
        else:
            delta = Delta(
                uploads=manifest_new.entries,
                deletions=[],
                changed={e.path: "new" for e in manifest_new.entries},
            )

        plan = self._build_plan(delta, manifest_new.root)
        plan["timestamp"] = ts
        plan["manifest"] = dump_manifest(manifest_new)

        # Write upload plan
        plan_path = self.output_dir / f"upload_{ts}.json"
        with open(plan_path, "w") as f:
            json.dump(plan, f, indent=2)
            f.write("\n")

        # Rotate manifests
        os.replace(self._manifest_new, self._manifest_prev)

        s = plan["summary"]
        if s["upload_count"] == 0 and s["deletion_count"] == 0:
            logger.info("[%s] No changes", ts)
        else:
            logger.info(
                "[%s] %d files to upload (%d bytes), %d deletions",
                ts, s["upload_count"], s["upload_bytes"], s["deletion_count"],
            )

        return plan

    def _build_plan(self, delta: Delta, root: str) -> dict:
        """Build an upload plan dict from a Delta."""
        files = []
        for entry in delta.uploads:
            needs_multipart = entry.size > self.multipart_threshold
            files.append({
                "path": entry.path,
                "full_path": os.path.join(root, entry.path),
                "size": entry.size,
                "sha256": entry.sha256,
                "category": entry.category,
                "reason": delta.changed.get(entry.path, "unknown"),
                "multipart": needs_multipart,
            })

        files.sort(key=lambda f: f["size"], reverse=True)

        total_bytes = sum(f["size"] for f in files)
        multipart_count = sum(1 for f in files if f["multipart"])

        return {
            "files": files,
            "deletions": delta.deletions,
            "summary": {
                "upload_count": len(files),
                "upload_bytes": total_bytes,
                "deletion_count": len(delta.deletions),
                "multipart_count": multipart_count,
            },
        }
