"""SQLite-backed project file and change tracking.

One instance per project. Stores file metadata and change history.
Actual file content lives on disk at ~/.brynq/projects/{id}/files/.
"""

import json
import logging
import os
import hashlib
import shutil
import sqlite3
import time
import uuid
from pathlib import Path
from typing import Any, Dict, List, Optional

logger = logging.getLogger("brynq.rooms.project_store")

PROJECTS_DIR = Path.home() / ".brynq" / "projects"

_SCHEMA = """
CREATE TABLE IF NOT EXISTS files (
    file_id         TEXT PRIMARY KEY,
    project_id      TEXT NOT NULL,
    relative_path   TEXT NOT NULL UNIQUE,
    size_bytes      INTEGER NOT NULL DEFAULT 0,
    content_hash    TEXT NOT NULL DEFAULT '',
    last_modified_by TEXT NOT NULL DEFAULT '',
    last_modified_at TEXT NOT NULL,
    is_deleted       INTEGER NOT NULL DEFAULT 0
);

CREATE INDEX IF NOT EXISTS idx_files_path ON files(relative_path);

CREATE TABLE IF NOT EXISTS changes (
    change_id       TEXT PRIMARY KEY,
    project_id      TEXT NOT NULL,
    file_id         TEXT NOT NULL,
    relative_path   TEXT NOT NULL,
    action          TEXT NOT NULL,
    user_id         TEXT NOT NULL DEFAULT '',
    user_name       TEXT NOT NULL DEFAULT '',
    content_hash    TEXT NOT NULL DEFAULT '',
    previous_hash   TEXT NOT NULL DEFAULT '',
    size_bytes      INTEGER NOT NULL DEFAULT 0,
    timestamp       TEXT NOT NULL,
    detail          TEXT NOT NULL DEFAULT ''
);

CREATE INDEX IF NOT EXISTS idx_changes_time ON changes(timestamp);
CREATE INDEX IF NOT EXISTS idx_changes_file ON changes(file_id);
"""


class ProjectStore:
    """Manages file metadata and change history for a single project."""

    def __init__(self, project_id: str, projects_dir: Optional[Path] = None):
        self.project_id = project_id
        self._projects_root = projects_dir or PROJECTS_DIR
        self._project_dir = self._projects_root / project_id
        self._files_dir = self._project_dir / "files"
        self._db_path = self._project_dir / "project.db"
        self._meta_path = self._project_dir / "project.json"

        # Ensure dirs exist
        self._files_dir.mkdir(parents=True, exist_ok=True)

        # Init DB
        self._conn = sqlite3.connect(str(self._db_path))
        self._conn.row_factory = sqlite3.Row
        self._conn.execute("PRAGMA journal_mode=WAL")
        self._conn.executescript(_SCHEMA)
        self._conn.commit()
        logger.info("ProjectStore initialized: %s", self._project_dir)

    @property
    def files_dir(self) -> Path:
        return self._files_dir

    # ── Project metadata ─────────────────────────────────────────

    def save_metadata(self, meta: Dict[str, Any]) -> None:
        """Write project.json."""
        with open(self._meta_path, "w", encoding="utf-8") as f:
            json.dump(meta, f, indent=2)

    def load_metadata(self) -> Dict[str, Any]:
        """Read project.json, or empty dict if missing."""
        if self._meta_path.exists():
            with open(self._meta_path, "r", encoding="utf-8") as f:
                return json.load(f)
        return {}

    # ── File operations ──────────────────────────────────────────

    def add_file(self, relative_path: str, content: bytes,
                 user_id: str = "", user_name: str = "") -> Dict[str, Any]:
        """Add or overwrite a file. Returns file metadata dict."""
        # Security: validate path
        safe_path = self._safe_relative_path(relative_path)
        if safe_path is None:
            raise ValueError(f"Invalid path: {relative_path}")

        # Write to disk
        full_path = self._files_dir / safe_path
        full_path.parent.mkdir(parents=True, exist_ok=True)
        full_path.write_bytes(content)

        # Compute hash
        content_hash = hashlib.sha256(content).hexdigest()
        size_bytes = len(content)
        now = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())

        # Check if file exists in DB
        existing = self._conn.execute(
            "SELECT file_id, content_hash FROM files WHERE relative_path = ? AND is_deleted = 0",
            (safe_path,)
        ).fetchone()

        if existing:
            file_id = existing["file_id"]
            previous_hash = existing["content_hash"]
            action = "update"
            self._conn.execute(
                "UPDATE files SET size_bytes=?, content_hash=?, last_modified_by=?, last_modified_at=? WHERE file_id=?",
                (size_bytes, content_hash, user_id, now, file_id)
            )
        else:
            file_id = f"file_{uuid.uuid4().hex[:12]}"
            previous_hash = ""
            action = "create"
            self._conn.execute(
                "INSERT INTO files (file_id, project_id, relative_path, size_bytes, content_hash, last_modified_by, last_modified_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
                (file_id, self.project_id, safe_path, size_bytes, content_hash, user_id, now)
            )

        # Record change
        change_id = f"chg_{uuid.uuid4().hex[:12]}"
        self._conn.execute(
            "INSERT INTO changes (change_id, project_id, file_id, relative_path, action, user_id, user_name, content_hash, previous_hash, size_bytes, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
            (change_id, self.project_id, file_id, safe_path, action, user_id, user_name, content_hash, previous_hash, size_bytes, now)
        )
        self._conn.commit()

        return {
            "file_id": file_id,
            "relative_path": safe_path,
            "content_hash": content_hash,
            "size_bytes": size_bytes,
            "action": action,
            "change_id": change_id,
            "timestamp": now,
        }

    def read_file(self, relative_path: str) -> Optional[bytes]:
        """Read file content from disk. Returns None if missing."""
        safe_path = self._safe_relative_path(relative_path)
        if safe_path is None:
            return None
        full_path = self._files_dir / safe_path
        if full_path.exists():
            return full_path.read_bytes()
        return None

    def delete_file(self, relative_path: str,
                    user_id: str = "", user_name: str = "") -> bool:
        """Soft-delete a file. Returns True if file existed."""
        safe_path = self._safe_relative_path(relative_path)
        if safe_path is None:
            return False

        existing = self._conn.execute(
            "SELECT file_id, content_hash FROM files WHERE relative_path = ? AND is_deleted = 0",
            (safe_path,)
        ).fetchone()
        if not existing:
            return False

        now = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
        self._conn.execute(
            "UPDATE files SET is_deleted = 1, last_modified_by = ?, last_modified_at = ? WHERE file_id = ?",
            (user_id, now, existing["file_id"])
        )

        # Record change
        change_id = f"chg_{uuid.uuid4().hex[:12]}"
        self._conn.execute(
            "INSERT INTO changes (change_id, project_id, file_id, relative_path, action, user_id, user_name, content_hash, previous_hash, size_bytes, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
            (change_id, self.project_id, existing["file_id"], safe_path, "delete", user_id, user_name, "", existing["content_hash"], 0, now)
        )
        self._conn.commit()

        # Remove from disk
        full_path = self._files_dir / safe_path
        if full_path.exists():
            full_path.unlink()

        return True

    def list_files(self) -> List[Dict[str, Any]]:
        """List all non-deleted files with metadata."""
        rows = self._conn.execute(
            "SELECT file_id, relative_path, size_bytes, content_hash, last_modified_by, last_modified_at FROM files WHERE is_deleted = 0 ORDER BY relative_path",
            ()
        ).fetchall()
        return [dict(r) for r in rows]

    def get_file_meta(self, relative_path: str) -> Optional[Dict[str, Any]]:
        """Get metadata for a single file."""
        safe_path = self._safe_relative_path(relative_path)
        if safe_path is None:
            return None
        row = self._conn.execute(
            "SELECT file_id, relative_path, size_bytes, content_hash, last_modified_by, last_modified_at FROM files WHERE relative_path = ? AND is_deleted = 0",
            (safe_path,)
        ).fetchone()
        return dict(row) if row else None

    # ── Manifest (for sync) ──────────────────────────────────────

    def get_manifest(self) -> List[Dict[str, str]]:
        """Return [{relative_path, content_hash, last_modified_at}, ...] for all live files.

        This is the payload sent during project_sync to let the other
        side figure out which files it needs to request.
        """
        rows = self._conn.execute(
            "SELECT relative_path, content_hash, last_modified_at FROM files WHERE is_deleted = 0 ORDER BY relative_path",
            ()
        ).fetchall()
        return [dict(r) for r in rows]

    # ── Change history ───────────────────────────────────────────

    def get_history(self, limit: int = 100) -> List[Dict[str, Any]]:
        """Get recent changes, newest first."""
        limit = max(1, min(limit, 500))
        rows = self._conn.execute(
            "SELECT change_id, file_id, relative_path, action, user_id, user_name, content_hash, previous_hash, size_bytes, timestamp, detail FROM changes ORDER BY timestamp DESC LIMIT ?",
            (limit,)
        ).fetchall()
        return [dict(r) for r in rows]

    # ── Path safety ──────────────────────────────────────────────

    def _safe_relative_path(self, relative_path: str) -> Optional[str]:
        """Validate and normalize a relative path. Returns None if unsafe.

        Rules:
        - No '..' components
        - No absolute paths
        - Normalize to forward slashes
        - Strip leading slashes
        - No null bytes
        """
        if not relative_path or "\x00" in relative_path:
            return None

        # Block absolute paths
        if Path(relative_path).is_absolute():
            return None
        if relative_path.startswith("/") or relative_path.startswith("\\"):
            return None

        # Normalize separators
        normalized = relative_path.replace("\\", "/").strip("/")

        # Block traversal
        parts = normalized.split("/")
        if ".." in parts:
            return None

        # Block absolute paths (Windows drive letters, UNC) - redundant but safe
        if len(normalized) >= 2 and normalized[1] == ":":
            return None
        if normalized.startswith("//"):
            return None

        # Block empty result
        if not normalized:
            return None

        return normalized

    # ── Lifecycle ────────────────────────────────────────────────

    def close(self):
        """Close the database connection."""
        self._conn.close()

    @staticmethod
    def delete_project(project_id: str, projects_dir: Optional[Path] = None) -> bool:
        """Permanently delete a project directory. Returns True if it existed."""
        root = projects_dir or PROJECTS_DIR
        project_dir = root / project_id
        if project_dir.exists():
            shutil.rmtree(project_dir)
            return True
        return False

    @staticmethod
    def list_projects(projects_dir: Optional[Path] = None) -> List[Dict[str, Any]]:
        """List all local projects by reading their project.json files."""
        projects = []
        root = projects_dir or PROJECTS_DIR
        if not root.exists():
            return projects
        for entry in sorted(root.iterdir()):
            if not entry.is_dir():
                continue
            meta_path = entry / "project.json"
            if meta_path.exists():
                try:
                    with open(meta_path, "r", encoding="utf-8") as f:
                        projects.append(json.load(f))
                except Exception:
                    pass
        return projects
