import mimetypes
import os
import threading
import uuid
from dataclasses import dataclass
from typing import Any

try:  # pragma: no cover - optional in some test paths
    import psycopg
    from psycopg.rows import dict_row
except Exception:  # pragma: no cover
    psycopg = None
    dict_row = None


MAX_TEXT_SIZE = 8 * 1024 * 1024
PREVIEW_TEXT_SIZE = 2 * 1024 * 1024

TEXT_EXTENSIONS = {
    "md", "markdown", "txt", "json", "js", "jsx", "ts", "tsx",
    "css", "scss", "less", "html", "htm", "py", "vue", "xml",
    "yaml", "yml", "csv", "log", "sql", "toml", "ini", "cfg",
    "conf", "sh", "bat", "ps1", "java", "c", "cpp", "h", "hpp",
    "go", "rs", "swift", "kt", "rb", "php", "pl", "r", "scala",
    "gradle", "properties", "env", "gitignore", "dockerfile",
}

BINARY_EXTENSIONS = {
    "exe", "dll", "so", "dylib", "bin", "dat", "obj", "o", "a",
    "doc", "docx", "xls", "xlsx", "ppt", "pptx", "odt", "ods", "odp",
    "pdf", "zip", "rar", "7z", "tar", "gz", "bz2", "xz",
    "png", "jpg", "jpeg", "gif", "bmp", "ico", "webp", "svg",
    "mp3", "mp4", "wav", "avi", "mov", "mkv", "flv", "wmv",
    "ttf", "otf", "woff", "woff2", "eot",
    "class", "pyc", "pyo", "jar", "war", "ear",
    "db", "sqlite", "mdb", "accdb",
    "pem", "key", "crt", "cer", "p12", "pfx", "jks",
    "msg", "eml", "pst", "ost",
    "dwg", "dxf", "step", "stl", "fbx", "3ds", "blend",
}

DEFAULT_MIME_TYPES = {
    "avi": "video/x-msvideo",
    "md": "text/markdown",
    "markdown": "text/markdown",
    "mkv": "video/x-matroska",
    "mov": "video/quicktime",
    "mp4": "video/mp4",
    "txt": "text/plain",
    "json": "application/json",
    "js": "text/javascript",
    "jsx": "text/javascript",
    "ts": "text/typescript",
    "tsx": "text/typescript",
    "css": "text/css",
    "html": "text/html",
    "htm": "text/html",
    "py": "text/x-python",
    "vue": "text/plain",
    "xml": "application/xml",
    "yaml": "text/yaml",
    "yml": "text/yaml",
    "csv": "text/csv",
    "log": "text/plain",
    "sql": "text/plain",
    "toml": "text/plain",
    "ini": "text/plain",
    "cfg": "text/plain",
    "conf": "text/plain",
    "sh": "text/plain",
    "bat": "text/plain",
    "ps1": "text/plain",
    "jpg": "image/jpeg",
    "jpeg": "image/jpeg",
    "flv": "video/x-flv",
    "m4v": "video/x-m4v",
    "png": "image/png",
    "gif": "image/gif",
    "webp": "image/webp",
    "svg": "image/svg+xml",
    "pdf": "application/pdf",
    "ogg": "video/ogg",
    "ogv": "video/ogg",
    "webm": "video/webm",
    "wmv": "video/x-ms-wmv",
}


def _now_sql() -> str:
    return "CURRENT_TIMESTAMP"


def get_extension(name: str = "") -> str:
    value = str(name or "")
    if "." not in value:
        return value.lower() if value.lower() == "dockerfile" else ""
    return value.rsplit(".", 1)[-1].lower()


def infer_mime_type(name: str, fallback: str = "") -> str:
    if fallback:
        return fallback
    ext = get_extension(name)
    guessed = DEFAULT_MIME_TYPES.get(ext)
    if guessed:
        return guessed
    guessed, _ = mimetypes.guess_type(name)
    return guessed or "application/octet-stream"


def is_text_file(name: str, mime_type: str = "") -> bool:
    ext = get_extension(name)
    if ext in BINARY_EXTENSIONS:
        return False
    mime_value = str(mime_type or "").lower()
    return ext in TEXT_EXTENSIONS or mime_value.startswith("text/") or "json" in mime_value or "xml" in mime_value


def _text_payload(text: str) -> dict[str, Any]:
    encoded = text.encode("utf-8")
    preview = encoded[:PREVIEW_TEXT_SIZE].decode("utf-8", errors="ignore")
    return {
        "storage_kind": "text",
        "size": len(encoded),
        "content_text": text,
        "preview_text": preview if len(encoded) > PREVIEW_TEXT_SIZE else text,
        "is_truncated_preview": len(encoded) > PREVIEW_TEXT_SIZE,
        "blob_data": None,
    }


def prepare_file_payload(name: str, raw_bytes: bytes, mime_type: str = "") -> dict[str, Any]:
    resolved_mime = infer_mime_type(name, mime_type)
    if is_text_file(name, resolved_mime):
        text = raw_bytes.decode("utf-8", errors="ignore")
        return {
            "mime_type": resolved_mime,
            **_text_payload(text),
        }
    return {
        "mime_type": resolved_mime,
        "storage_kind": "blob",
        "size": len(raw_bytes),
        "content_text": "",
        "preview_text": "",
        "is_truncated_preview": False,
        "blob_data": raw_bytes,
    }


@dataclass
class BlobPayload:
    content: bytes
    mime_type: str
    filename: str


class BaseDocumentStore:
    _UNSET = object()

    def list_nodes(self) -> list[dict[str, Any]]:
        raise NotImplementedError

    def create_folder(self, name: str, parent_id: str | None) -> dict[str, Any]:
        raise NotImplementedError

    def create_text_file(self, name: str, parent_id: str | None, content: str = "") -> dict[str, Any]:
        raise NotImplementedError

    def upload_file(self, name: str, parent_id: str | None, raw_bytes: bytes, mime_type: str = "") -> dict[str, Any]:
        raise NotImplementedError

    def update_node(self, node_id: str, *, name: str | None | object = _UNSET, parent_id: str | None | object = _UNSET, content: str | None | object = _UNSET) -> dict[str, Any]:
        raise NotImplementedError

    def replace_blob(self, node_id: str, filename: str, raw_bytes: bytes, mime_type: str = "") -> dict[str, Any]:
        raise NotImplementedError

    def delete_node(self, node_id: str) -> None:
        raise NotImplementedError

    def get_blob(self, node_id: str) -> BlobPayload:
        raise NotImplementedError


class InMemoryDocumentStore(BaseDocumentStore):
    def __init__(self) -> None:
        self.nodes: dict[str, dict[str, Any]] = {}

    def _serialize(self, node: dict[str, Any]) -> dict[str, Any]:
        return {
            "id": node["id"],
            "parentId": node["parent_id"],
            "type": node["type"],
            "name": node["name"],
            "mimeType": node.get("mime_type") or "",
            "storageKind": node.get("storage_kind") or "text",
            "size": int(node.get("size") or 0),
            "content": node.get("content_text") or "",
            "previewText": node.get("preview_text") or "",
            "isTruncatedPreview": bool(node.get("is_truncated_preview")),
            "createdAt": int(node["created_at"]),
            "updatedAt": int(node["updated_at"]),
        }

    def _timestamp(self) -> int:
        import time
        return int(time.time() * 1000)

    def _get(self, node_id: str) -> dict[str, Any]:
        node = self.nodes.get(node_id)
        if node is None:
            raise KeyError(node_id)
        return node

    def list_nodes(self) -> list[dict[str, Any]]:
        ordered = sorted(
            self.nodes.values(),
            key=lambda item: (item["type"] != "folder", item["name"].lower(), item["created_at"]),
        )
        return [self._serialize(node) for node in ordered]

    def create_folder(self, name: str, parent_id: str | None) -> dict[str, Any]:
        now = self._timestamp()
        node = {
            "id": str(uuid.uuid4()),
            "parent_id": parent_id,
            "type": "folder",
            "name": name,
            "mime_type": "",
            "storage_kind": "text",
            "size": 0,
            "content_text": "",
            "preview_text": "",
            "is_truncated_preview": False,
            "blob_data": None,
            "created_at": now,
            "updated_at": now,
        }
        self.nodes[node["id"]] = node
        return self._serialize(node)

    def create_text_file(self, name: str, parent_id: str | None, content: str = "") -> dict[str, Any]:
        now = self._timestamp()
        payload = _text_payload(content)
        node = {
            "id": str(uuid.uuid4()),
            "parent_id": parent_id,
            "type": "file",
            "name": name,
            "mime_type": infer_mime_type(name),
            **payload,
            "created_at": now,
            "updated_at": now,
        }
        self.nodes[node["id"]] = node
        return self._serialize(node)

    def upload_file(self, name: str, parent_id: str | None, raw_bytes: bytes, mime_type: str = "") -> dict[str, Any]:
        now = self._timestamp()
        payload = prepare_file_payload(name, raw_bytes, mime_type)
        node = {
            "id": str(uuid.uuid4()),
            "parent_id": parent_id,
            "type": "file",
            "name": name,
            **payload,
            "created_at": now,
            "updated_at": now,
        }
        self.nodes[node["id"]] = node
        return self._serialize(node)

    def update_node(self, node_id: str, *, name: str | None | object = BaseDocumentStore._UNSET, parent_id: str | None | object = BaseDocumentStore._UNSET, content: str | None | object = BaseDocumentStore._UNSET) -> dict[str, Any]:
        node = self._get(node_id)
        if name is not BaseDocumentStore._UNSET:
            node["name"] = name
            if node["type"] == "file":
                node["mime_type"] = infer_mime_type(name, node.get("mime_type") or "")
        if parent_id is not BaseDocumentStore._UNSET:
            node["parent_id"] = parent_id
        if content is not BaseDocumentStore._UNSET:
            payload = _text_payload(content)
            node.update(payload)
        node["updated_at"] = self._timestamp()
        return self._serialize(node)

    def replace_blob(self, node_id: str, filename: str, raw_bytes: bytes, mime_type: str = "") -> dict[str, Any]:
        node = self._get(node_id)
        payload = prepare_file_payload(filename, raw_bytes, mime_type)
        node.update(payload)
        node["name"] = filename
        node["mime_type"] = payload["mime_type"]
        node["updated_at"] = self._timestamp()
        return self._serialize(node)

    def delete_node(self, node_id: str) -> None:
        descendants = {node_id}
        changed = True
        while changed:
            changed = False
            for node in list(self.nodes.values()):
                if node.get("parent_id") in descendants and node["id"] not in descendants:
                    descendants.add(node["id"])
                    changed = True
        for current_id in descendants:
            self.nodes.pop(current_id, None)

    def get_blob(self, node_id: str) -> BlobPayload:
        node = self._get(node_id)
        if node["type"] != "file":
            raise FileNotFoundError(node_id)
        if node.get("blob_data") is not None:
            content = node["blob_data"]
        else:
            content = (node.get("content_text") or "").encode("utf-8")
        return BlobPayload(
            content=content,
            mime_type=node.get("mime_type") or infer_mime_type(node.get("name") or ""),
            filename=node["name"],
        )


class PostgresDocumentStore(BaseDocumentStore):
    def __init__(self, database_url: str) -> None:
        if psycopg is None or dict_row is None:
            raise RuntimeError("psycopg 未安装，无法使用 PostgreSQL 文档存储")
        self.database_url = database_url
        self._init_lock = threading.Lock()
        self._initialized = False

    def _connect(self):
        return psycopg.connect(self.database_url, autocommit=True, row_factory=dict_row)

    def _ensure_initialized(self) -> None:
        if self._initialized:
            return
        with self._init_lock:
            if self._initialized:
                return
            with self._connect() as conn:
                with conn.cursor() as cur:
                    cur.execute(
                        """
                        CREATE TABLE IF NOT EXISTS document_nodes (
                            id TEXT PRIMARY KEY,
                            parent_id TEXT REFERENCES document_nodes(id) ON DELETE CASCADE,
                            type TEXT NOT NULL CHECK (type IN ('folder', 'file')),
                            name TEXT NOT NULL,
                            mime_type TEXT NOT NULL DEFAULT '',
                            storage_kind TEXT NOT NULL DEFAULT 'text',
                            size BIGINT NOT NULL DEFAULT 0,
                            content_text TEXT NOT NULL DEFAULT '',
                            preview_text TEXT NOT NULL DEFAULT '',
                            is_truncated_preview BOOLEAN NOT NULL DEFAULT FALSE,
                            blob_data BYTEA,
                            created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
                            updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
                        )
                        """
                    )
                    cur.execute(
                        "CREATE INDEX IF NOT EXISTS document_nodes_parent_idx ON document_nodes(parent_id)"
                    )
            self._initialized = True

    def _serialize(self, row: dict[str, Any]) -> dict[str, Any]:
        created = row["created_at"]
        updated = row["updated_at"]
        return {
            "id": row["id"],
            "parentId": row["parent_id"],
            "type": row["type"],
            "name": row["name"],
            "mimeType": row.get("mime_type") or "",
            "storageKind": row.get("storage_kind") or "text",
            "size": int(row.get("size") or 0),
            "content": row.get("content_text") or "",
            "previewText": row.get("preview_text") or "",
            "isTruncatedPreview": bool(row.get("is_truncated_preview")),
            "createdAt": int(created.timestamp() * 1000),
            "updatedAt": int(updated.timestamp() * 1000),
        }

    def _fetch_node(self, node_id: str) -> dict[str, Any]:
        self._ensure_initialized()
        with self._connect() as conn:
            with conn.cursor() as cur:
                cur.execute("SELECT * FROM document_nodes WHERE id = %s", (node_id,))
                row = cur.fetchone()
                if row is None:
                    raise KeyError(node_id)
                return row

    def list_nodes(self) -> list[dict[str, Any]]:
        self._ensure_initialized()
        with self._connect() as conn:
            with conn.cursor() as cur:
                cur.execute(
                    """
                    SELECT
                        id,
                        parent_id,
                        type,
                        name,
                        mime_type,
                        storage_kind,
                        size,
                        CASE
                            WHEN storage_kind = 'text' AND size <= %s THEN content_text
                            ELSE ''
                        END AS content_text,
                        preview_text,
                        is_truncated_preview,
                        created_at,
                        updated_at
                    FROM document_nodes
                    ORDER BY
                        CASE WHEN type = 'folder' THEN 0 ELSE 1 END,
                        LOWER(name),
                        created_at
                    """,
                    (MAX_TEXT_SIZE,),
                )
                return [self._serialize(row) for row in cur.fetchall()]

    def create_folder(self, name: str, parent_id: str | None) -> dict[str, Any]:
        self._ensure_initialized()
        node_id = str(uuid.uuid4())
        with self._connect() as conn:
            with conn.cursor() as cur:
                cur.execute(
                    """
                    INSERT INTO document_nodes (id, parent_id, type, name, updated_at)
                    VALUES (%s, %s, 'folder', %s, CURRENT_TIMESTAMP)
                    RETURNING *
                    """,
                    (node_id, parent_id, name),
                )
                return self._serialize(cur.fetchone())

    def create_text_file(self, name: str, parent_id: str | None, content: str = "") -> dict[str, Any]:
        self._ensure_initialized()
        node_id = str(uuid.uuid4())
        payload = _text_payload(content)
        with self._connect() as conn:
            with conn.cursor() as cur:
                cur.execute(
                    """
                    INSERT INTO document_nodes (
                        id, parent_id, type, name, mime_type, storage_kind, size,
                        content_text, preview_text, is_truncated_preview, blob_data, updated_at
                    )
                    VALUES (%s, %s, 'file', %s, %s, %s, %s, %s, %s, %s, %s, CURRENT_TIMESTAMP)
                    RETURNING *
                    """,
                    (
                        node_id,
                        parent_id,
                        name,
                        infer_mime_type(name),
                        payload["storage_kind"],
                        payload["size"],
                        payload["content_text"],
                        payload["preview_text"],
                        payload["is_truncated_preview"],
                        None,
                    ),
                )
                return self._serialize(cur.fetchone())

    def upload_file(self, name: str, parent_id: str | None, raw_bytes: bytes, mime_type: str = "") -> dict[str, Any]:
        self._ensure_initialized()
        node_id = str(uuid.uuid4())
        payload = prepare_file_payload(name, raw_bytes, mime_type)
        with self._connect() as conn:
            with conn.cursor() as cur:
                cur.execute(
                    """
                    INSERT INTO document_nodes (
                        id, parent_id, type, name, mime_type, storage_kind, size,
                        content_text, preview_text, is_truncated_preview, blob_data, updated_at
                    )
                    VALUES (%s, %s, 'file', %s, %s, %s, %s, %s, %s, %s, %s, CURRENT_TIMESTAMP)
                    RETURNING *
                    """,
                    (
                        node_id,
                        parent_id,
                        name,
                        payload["mime_type"],
                        payload["storage_kind"],
                        payload["size"],
                        payload["content_text"],
                        payload["preview_text"],
                        payload["is_truncated_preview"],
                        payload["blob_data"],
                    ),
                )
                return self._serialize(cur.fetchone())

    def update_node(self, node_id: str, *, name: str | None | object = BaseDocumentStore._UNSET, parent_id: str | None | object = BaseDocumentStore._UNSET, content: str | None | object = BaseDocumentStore._UNSET) -> dict[str, Any]:
        self._ensure_initialized()
        row = self._fetch_node(node_id)
        next_name = row["name"] if name is BaseDocumentStore._UNSET else name
        next_parent_id = row["parent_id"] if parent_id is BaseDocumentStore._UNSET else parent_id
        next_mime_type = row.get("mime_type") or ""
        next_storage_kind = row.get("storage_kind") or "text"
        next_size = row.get("size") or 0
        next_content_text = row.get("content_text") or ""
        next_preview_text = row.get("preview_text") or ""
        next_is_truncated = bool(row.get("is_truncated_preview"))
        next_blob_data = row.get("blob_data")

        if row["type"] == "file" and name is not BaseDocumentStore._UNSET:
            next_mime_type = infer_mime_type(next_name, next_mime_type)
        if content is not BaseDocumentStore._UNSET:
            payload = _text_payload(content)
            next_storage_kind = payload["storage_kind"]
            next_size = payload["size"]
            next_content_text = payload["content_text"]
            next_preview_text = payload["preview_text"]
            next_is_truncated = payload["is_truncated_preview"]
            next_blob_data = None

        with self._connect() as conn:
            with conn.cursor() as cur:
                cur.execute(
                    """
                    UPDATE document_nodes
                    SET
                        name = %s,
                        parent_id = %s,
                        mime_type = %s,
                        storage_kind = %s,
                        size = %s,
                        content_text = %s,
                        preview_text = %s,
                        is_truncated_preview = %s,
                        blob_data = %s,
                        updated_at = CURRENT_TIMESTAMP
                    WHERE id = %s
                    RETURNING *
                    """,
                    (
                        next_name,
                        next_parent_id,
                        next_mime_type,
                        next_storage_kind,
                        next_size,
                        next_content_text,
                        next_preview_text,
                        next_is_truncated,
                        next_blob_data,
                        node_id,
                    ),
                )
                updated = cur.fetchone()
                if updated is None:
                    raise KeyError(node_id)
                return self._serialize(updated)

    def replace_blob(self, node_id: str, filename: str, raw_bytes: bytes, mime_type: str = "") -> dict[str, Any]:
        self._ensure_initialized()
        payload = prepare_file_payload(filename, raw_bytes, mime_type)
        with self._connect() as conn:
            with conn.cursor() as cur:
                cur.execute(
                    """
                    UPDATE document_nodes
                    SET
                        name = %s,
                        mime_type = %s,
                        storage_kind = %s,
                        size = %s,
                        content_text = %s,
                        preview_text = %s,
                        is_truncated_preview = %s,
                        blob_data = %s,
                        updated_at = CURRENT_TIMESTAMP
                    WHERE id = %s AND type = 'file'
                    RETURNING *
                    """,
                    (
                        filename,
                        payload["mime_type"],
                        payload["storage_kind"],
                        payload["size"],
                        payload["content_text"],
                        payload["preview_text"],
                        payload["is_truncated_preview"],
                        payload["blob_data"],
                        node_id,
                    ),
                )
                updated = cur.fetchone()
                if updated is None:
                    raise KeyError(node_id)
                return self._serialize(updated)

    def delete_node(self, node_id: str) -> None:
        self._ensure_initialized()
        with self._connect() as conn:
            with conn.cursor() as cur:
                cur.execute(
                    """
                    WITH RECURSIVE descendants AS (
                        SELECT id FROM document_nodes WHERE id = %s
                        UNION ALL
                        SELECT child.id
                        FROM document_nodes child
                        INNER JOIN descendants parent ON child.parent_id = parent.id
                    )
                    DELETE FROM document_nodes
                    WHERE id IN (SELECT id FROM descendants)
                    """,
                    (node_id,),
                )

    def get_blob(self, node_id: str) -> BlobPayload:
        self._ensure_initialized()
        with self._connect() as conn:
            with conn.cursor() as cur:
                cur.execute(
                    """
                    SELECT id, name, mime_type, content_text, blob_data
                    FROM document_nodes
                    WHERE id = %s AND type = 'file'
                    """,
                    (node_id,),
                )
                row = cur.fetchone()
                if row is None:
                    raise FileNotFoundError(node_id)
                blob_data = row.get("blob_data")
                if blob_data is None:
                    content = (row.get("content_text") or "").encode("utf-8")
                else:
                    content = bytes(blob_data)
                return BlobPayload(
                    content=content,
                    mime_type=row.get("mime_type") or infer_mime_type(row.get("name") or ""),
                    filename=row["name"],
                )


_document_store: BaseDocumentStore | None = None


def get_document_store() -> BaseDocumentStore:
    global _document_store
    if _document_store is not None:
        return _document_store
    backend = (os.getenv("DOCS_BACKEND") or "postgres").strip().lower()
    if backend == "memory":
        _document_store = InMemoryDocumentStore()
        return _document_store

    database_url = os.getenv("DATABASE_URL", "").strip()
    if not database_url:
        raise RuntimeError("缺少 DATABASE_URL，无法初始化 PostgreSQL 文档存储")
    _document_store = PostgresDocumentStore(database_url)
    return _document_store


def reset_document_store() -> None:
    global _document_store
    _document_store = None
