from __future__ import annotations

import hashlib
import json
import shutil
from pathlib import Path
from typing import Any

from niannian.external_data.schema import utc_now_iso


def rel_path_for(project: str, function: str, record_id: str) -> str:
    return f"external/{project}/{function}/{record_id}.json"


def index_rel_path(project: str, function: str) -> str:
    return f"external/{project}/{function}/_index.jsonl"


class ExternalDataStore:
    """外部数据 L0 存储 — 不归 L2 dream 归档，仅 L1 三云同步。"""

    def __init__(self, home: Path) -> None:
        self._home = home

    def base_dir(self, project: str, function: str, *, storage_target: str, local_dir: str = "") -> Path:
        if storage_target == "local":
            root = Path(local_dir).expanduser()
            return root / project / function
        return self._home / "external" / project / function

    def record_path(self, project: str, function: str, record_id: str, *, storage_target: str, local_dir: str = "") -> Path:
        return self.base_dir(project, function, storage_target=storage_target, local_dir=local_dir) / f"{record_id}.json"

    def index_path(self, project: str, function: str, *, storage_target: str, local_dir: str = "") -> Path:
        return self.base_dir(project, function, storage_target=storage_target, local_dir=local_dir) / "_index.jsonl"

    def resolve_record_id(self, req_record_id: str, dedup_key: str, project: str, function: str) -> str:
        if req_record_id:
            return req_record_id
        if dedup_key:
            digest = hashlib.sha256(f"{project}:{function}:{dedup_key}".encode()).hexdigest()[:16]
            return f"dedup_{digest}"
        raise ValueError("record_id or dedup_key required")

    def find_by_dedup_key(self, project: str, function: str, dedup_key: str, *, storage_target: str, local_dir: str = "") -> str | None:
        idx = self.index_path(project, function, storage_target=storage_target, local_dir=local_dir)
        if not idx.is_file():
            return None
        for line in idx.read_text(encoding="utf-8").splitlines():
            if not line.strip():
                continue
            try:
                row = json.loads(line)
            except json.JSONDecodeError:
                continue
            if row.get("dedup_key") == dedup_key and not row.get("deleted"):
                return str(row.get("record_id") or "")
        return None

    def write_record(self, envelope: dict[str, Any], *, storage_target: str, local_dir: str = "") -> Path:
        project = envelope["project"]
        function = envelope["function"]
        record_id = envelope["record_id"]
        path = self.record_path(project, function, record_id, storage_target=storage_target, local_dir=local_dir)
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(json.dumps(envelope, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
        self._append_index(envelope, storage_target=storage_target, local_dir=local_dir)
        return path

    def delete_record(self, project: str, function: str, record_id: str, *, storage_target: str, local_dir: str = "") -> bool:
        path = self.record_path(project, function, record_id, storage_target=storage_target, local_dir=local_dir)
        existed = path.is_file()
        if existed:
            path.unlink()
        self._append_index(
            {
                "record_id": record_id,
                "project": project,
                "function": function,
                "operation": "delete",
                "deleted": True,
                "occurred_at": utc_now_iso(),
            },
            storage_target=storage_target,
            local_dir=local_dir,
        )
        return existed

    def _append_index(self, row: dict[str, Any], *, storage_target: str, local_dir: str = "") -> None:
        project = row["project"]
        function = row["function"]
        idx = self.index_path(project, function, storage_target=storage_target, local_dir=local_dir)
        idx.parent.mkdir(parents=True, exist_ok=True)
        with idx.open("a", encoding="utf-8") as f:
            f.write(json.dumps(row, ensure_ascii=False) + "\n")

    def manifest_rels(self, project: str, function: str) -> list[str]:
        """cloud 模式下需同步的 manifest 相对路径。"""
        base = self._home / "external" / project / function
        if not base.is_dir():
            return []
        rels: list[str] = []
        for p in sorted(base.iterdir()):
            if p.is_file():
                rels.append(f"external/{project}/{function}/{p.name}")
        return rels

    def archive_to_l2(
        self,
        project: str,
        function: str,
        archive_root: Path,
        *,
        storage_target: str = "cloud",
        local_dir: str = "",
    ) -> dict[str, Any]:
        """Chat 触发的 L2 归档：{archive_root}/{project}/{function}/"""
        src = self.base_dir(project, function, storage_target=storage_target, local_dir=local_dir)
        if not src.is_dir():
            return {"ok": False, "error": "source_not_found", "path": str(src)}
        dest = archive_root / project / function
        dest.mkdir(parents=True, exist_ok=True)
        copied: list[str] = []
        for p in src.iterdir():
            if not p.is_file():
                continue
            target = dest / p.name
            shutil.copy2(p, target)
            copied.append(p.name)
            if p.suffix == ".json" and p.name != "_index.jsonl":
                try:
                    data = json.loads(p.read_text(encoding="utf-8"))
                    if isinstance(data, dict):
                        data["archived"] = True
                        data["archived_at"] = utc_now_iso()
                        p.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
                except (json.JSONDecodeError, OSError):
                    pass
        return {"ok": True, "dest": str(dest), "files": copied, "count": len(copied)}
