import hashlib
import json
import shutil
import time
import uuid
from datetime import datetime
from pathlib import Path
from typing import Dict, List
from pypos.core.utils.db_helper import open_sqlite_connection

# edited by glg


class DrRestoreDrillService:
    def __init__(self, work_root: str = ""):
        self.work_root = Path(str(work_root or "")).resolve() if str(work_root or "").strip() else None

    @staticmethod
    def _sha256_file(path: Path) -> str:
        h = hashlib.sha256()
        with path.open("rb") as f:
            for chunk in iter(lambda: f.read(1024 * 1024), b""):
                h.update(chunk)
        return h.hexdigest()

    # edited by glg
    @staticmethod
    def _new_trace_id(scope: str = "dr_restore") -> str:
        return f"ops-{str(scope or 'dr_restore').strip().lower()}-{uuid.uuid4().hex[:10]}"

    @staticmethod
    def _read_manifest(manifest_path: str) -> Dict:
        path = Path(str(manifest_path or "")).resolve()
        if not path.exists() or not path.is_file():
            raise FileNotFoundError(f"Manifest tidak ditemukan: {path}")
        payload = json.loads(path.read_text(encoding="utf-8"))
        if not isinstance(payload, dict):
            raise ValueError("Manifest harus berupa object JSON.")
        copied = payload.get("copied_files")
        if not isinstance(copied, list):
            raise ValueError("Manifest tidak memiliki copied_files yang valid.")
        payload["manifest_path"] = str(path)
        return payload

    @staticmethod
    def _resolve_backup_file(entry: Dict, backup_dir: Path) -> Path:
        explicit_target = str(entry.get("target") or "").strip()
        if explicit_target:
            target = Path(explicit_target).expanduser().resolve()
            if target.exists():
                return target
        source_name = Path(str(entry.get("source") or "unknown")).name
        return (backup_dir / source_name).resolve()

    @staticmethod
    def _run_sqlite_smoke(db_path: Path) -> Dict:
        try:
            conn = open_sqlite_connection(
                db_path=str(db_path),
                timeout=3,
                uri=False,
                apply_pragmas=False,
                ensure_indexes=False,
                run_migrations=False,
            )
            cur = conn.cursor()
            cur.execute("PRAGMA quick_check(1)")
            row = cur.fetchone()
            result = str(row[0] if row else "").strip().lower()
            conn.close()
            ok = result == "ok"
            return {
                "path": str(db_path),
                "ok": bool(ok),
                "message": "ok" if ok else (result or "quick_check_failed"),
            }
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError) as exc:
            return {
                "path": str(db_path),
                "ok": False,
                "message": str(exc),
            }

    def _build_restore_dir(self, manifest_path: Path, restore_root: str = "") -> Path:
        root_raw = str(restore_root or "").strip()
        if root_raw:
            root = Path(root_raw).expanduser().resolve()
        elif self.work_root is not None:
            root = self.work_root
        else:
            root = manifest_path.parent / "restore_drill"
        ts = datetime.now().strftime("%Y%m%d_%H%M%S")
        out_dir = (root / f"drill_{ts}").resolve()
        out_dir.mkdir(parents=True, exist_ok=True)
        return out_dir

    def run_restore_drill(self, manifest_path: str, restore_root: str = "") -> Dict:
        trace_id = self._new_trace_id("restore_drill")
        started = time.perf_counter()
        result = {
            "ok": False,
            "manifest_path": "",
            "backup_dir": "",
            "restore_dir": "",
            "checked_files": 0,
            "restored_files": [],
            "missing_files": [],
            "checksum_mismatches": [],
            "sqlite_smoke": [],
            "duration_ms": 0.0,
            "report_path": "",
            "error": "",
            "error_code": "",
            "reason": "ok",
            "trace_id": trace_id,
        }
        try:
            manifest = self._read_manifest(manifest_path)
            manifest_obj = Path(str(manifest["manifest_path"])).resolve()
            backup_dir = Path(str(manifest.get("backup_dir") or manifest_obj.parent)).resolve()
            copied_files = manifest.get("copied_files") if isinstance(manifest.get("copied_files"), list) else []
            out_dir = self._build_restore_dir(manifest_obj, restore_root=restore_root)

            result["manifest_path"] = str(manifest_obj)
            result["backup_dir"] = str(backup_dir)
            result["restore_dir"] = str(out_dir)

            for entry in copied_files:
                item = entry if isinstance(entry, dict) else {}
                src_backup = self._resolve_backup_file(item, backup_dir)
                expected_sha = str(item.get("sha256") or "").strip().lower()
                dst = out_dir / src_backup.name
                if not src_backup.exists() or not src_backup.is_file():
                    result["missing_files"].append(str(src_backup))
                    continue
                dst.parent.mkdir(parents=True, exist_ok=True)
                shutil.copy2(str(src_backup), str(dst))
                actual_sha = self._sha256_file(dst).lower()
                rec = {
                    "source_backup": str(src_backup),
                    "restored_path": str(dst),
                    "size": int(dst.stat().st_size if dst.exists() else 0),
                    "sha256": actual_sha,
                }
                result["restored_files"].append(rec)
                if expected_sha and actual_sha != expected_sha:
                    result["checksum_mismatches"].append(
                        {
                            "path": str(dst),
                            "expected": expected_sha,
                            "actual": actual_sha,
                        }
                    )

                if dst.suffix.lower() in {".db", ".sqlite", ".sqlite3"}:
                    result["sqlite_smoke"].append(self._run_sqlite_smoke(dst))

            result["checked_files"] = int(len(copied_files))
            sqlite_fail = [x for x in result["sqlite_smoke"] if not bool((x or {}).get("ok"))]
            result["ok"] = (
                len(result["missing_files"]) == 0
                and len(result["checksum_mismatches"]) == 0
                and len(sqlite_fail) == 0
            )

            report_payload = {
                "created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
                **result,
            }
            report_path = out_dir / "restore_drill_report.json"
            report_path.write_text(json.dumps(report_payload, ensure_ascii=False, indent=2), encoding="utf-8")
            result["report_path"] = str(report_path)
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError, LookupError, ArithmeticError, ImportError) as exc:
            result["error"] = str(exc)
            result["error_code"] = "OPS_DR_RESTORE_FAILED"
            result["reason"] = "restore_drill_failed"
        finally:
            result["duration_ms"] = round((time.perf_counter() - started) * 1000.0, 2)
        return result
