"""User and workspace management utilities for multi-user support."""

import json
import re
import shutil
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Dict, Optional, Tuple

from werkzeug.security import check_password_hash, generate_password_hash

from config import (
    ADMIN_PASSWORD_HASH,
    ADMIN_USERNAME,
    INVITE_CODES_FILE,
    USER_SPACE_DIR,
    USERS_DB_FILE,
    UPLOAD_QUARANTINE_SUBDIR,
    MAIN_MEMORY_FILE,
    TASK_MEMORY_FILE,
    IS_HOST_MODE,
    WEB_DATA_DIR,
    WEB_USER_SPACE_DIR,
)
from modules.personalization_manager import ensure_personalization_config
from modules.personalization_manager import PERSONALIZATION_FILENAME


@dataclass
class UserRecord:
    username: str
    email: str
    password_hash: str
    created_at: str
    invite_code: Optional[str] = None
    role: str = "user"
    tutorial_completed: bool = False
    default_workspace_id: str = "default"


@dataclass
class UserWorkspace:
    username: str
    root: Path
    project_path: Path
    data_dir: Path
    logs_dir: Path
    uploads_dir: Path
    quarantine_dir: Path


class UserManager:
    """Handle user registration, authentication and workspace provisioning."""

    USERNAME_REGEX = re.compile(r"^[a-z0-9_\-]{3,32}$")
    RESERVED_WORKSPACE_IDS = {"default", "personal", "shared"}

    def __init__(
        self,
        users_file: str = USERS_DB_FILE,
        invite_codes_file: str = INVITE_CODES_FILE,
        workspace_root: str = USER_SPACE_DIR,
    ):
        self.users_file = Path(users_file)
        self.invite_codes_file = Path(invite_codes_file)
        self.workspace_root = Path(workspace_root).expanduser().resolve()
        self.workspace_root.mkdir(parents=True, exist_ok=True)

        # host 模式下同时维护 web 路径引用（双数据源）
        self._web_workspace_root: Optional[Path] = None
        self._web_users_file: Optional[Path] = None
        if IS_HOST_MODE:
            self._web_workspace_root = Path(WEB_USER_SPACE_DIR).expanduser().resolve()
            self._web_users_file = Path(WEB_DATA_DIR) / "users.json"

        self._users: Dict[str, UserRecord] = {}
        self._invites: Dict[str, Dict] = {}
        self._email_map: Dict[str, str] = {}

        self._load_users()
        self._load_invite_codes()
        self._ensure_admin_user()

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------
    def register_user(
        self, username: str, email: str, password: str, invite_code: str
    ) -> Tuple[UserRecord, UserWorkspace]:
        username = self._normalize_username(username)
        email = self._normalize_email(email)
        password = password.strip()
        invite_code = (invite_code or "").strip()

        if not password or len(password) < 8:
            raise ValueError("密码长度至少 8 位。")
        if username in self._users:
            raise ValueError("该用户名已被注册。")
        if email in self._email_map:
            raise ValueError("该邮箱已被注册。")

        invite_entry = self._validate_invite_code(invite_code)
        password_hash = generate_password_hash(password)
        created_at = datetime.utcnow().isoformat()

        record = UserRecord(
            username=username,
            email=email,
            password_hash=password_hash,
            created_at=created_at,
            invite_code=invite_entry["code"],
            tutorial_completed=False,
        )
        self._users[username] = record
        self._index_user(record)
        self._save_users()
        self._consume_invite(invite_entry)

        workspace = self.ensure_user_workspace(username)
        return record, workspace

    def authenticate(self, email: str, password: str) -> Optional[UserRecord]:
        email = (email or "").strip().lower()
        username = self._email_map.get(email)
        if not username:
            return None
        record = self._users.get(username)
        if not record or not record.password_hash:
            return None
        if not check_password_hash(record.password_hash, password or ""):
            return None
        return record

    def get_user(self, username: str) -> Optional[UserRecord]:
        return self._users.get((username or "").strip().lower())

    @staticmethod
    def normalize_workspace_id(workspace_id: str) -> str:
        candidate = (workspace_id or "default").strip()
        if not candidate:
            candidate = "default"
        if not re.fullmatch(r"[A-Za-z0-9._-]{1,40}", candidate):
            raise ValueError("项目 ID 只能包含字母、数字、点、下划线或连字符，长度 1-40。")
        return candidate

    @classmethod
    def validate_new_workspace_id(cls, workspace_id: str) -> str:
        candidate = cls.normalize_workspace_id(workspace_id)
        if candidate in cls.RESERVED_WORKSPACE_IDS or candidate.startswith("."):
            raise ValueError("该项目名称已被系统保留，请换一个名称。")
        return candidate

    def _project_container_root(self, username: str) -> Path:
        """返回 host 模式 projects 容器目录（写操作始终走 host 路径）。"""
        return (self.workspace_root / username / "projects").resolve()

    def _personal_root(self, username: str) -> Path:
        return (self.workspace_root / username / "personal").resolve()

    @staticmethod
    def _looks_like_project_workspace_dir(path: Path) -> bool:
        return path.is_dir() and ((path / "project").exists() or (path / "data").exists())

    def _move_dir_contents(self, source: Path, target: Path) -> None:
        if not source.exists() or not source.is_dir():
            return
        target.mkdir(parents=True, exist_ok=True)
        for child in list(source.iterdir()):
            dest = target / child.name
            if dest.exists():
                continue
            shutil.move(str(child), str(dest))

    def _ensure_shared_file_link(self, link_path: Path, target_path: Path) -> None:
        target_path.parent.mkdir(parents=True, exist_ok=True)
        link_path.parent.mkdir(parents=True, exist_ok=True)
        if link_path.is_symlink():
            try:
                if link_path.resolve() == target_path.resolve():
                    return
            except Exception:
                pass
            try:
                link_path.unlink()
            except Exception:
                return
        if link_path.exists():
            if not link_path.is_file():
                return
            try:
                should_adopt_existing = (
                    not target_path.exists()
                    or (target_path.is_file() and target_path.stat().st_size == 0)
                )
            except Exception:
                should_adopt_existing = not target_path.exists()
            if should_adopt_existing:
                try:
                    shutil.copy2(str(link_path), str(target_path))
                except Exception:
                    return
            try:
                link_path.unlink()
            except Exception:
                return
        if not target_path.exists():
            target_path.touch()
        try:
            link_path.symlink_to(target_path)
        except Exception:
            if not link_path.exists() and target_path.exists():
                try:
                    shutil.copy2(target_path, link_path)
                except Exception:
                    pass

    def _ensure_shared_user_state_links(self, username: str, data_dir: Path) -> None:
        """项目间共享用户级个性化与记忆，同时保持 data_dir 路径兼容。"""
        personal_dir = self._personal_root(username)
        personalization_dir = personal_dir / "personalization"
        memory_dir = personal_dir / "memory"
        personalization_dir.mkdir(parents=True, exist_ok=True)
        memory_dir.mkdir(parents=True, exist_ok=True)

        self._ensure_shared_file_link(
            data_dir / PERSONALIZATION_FILENAME,
            personalization_dir / PERSONALIZATION_FILENAME,
        )
        ensure_personalization_config(personalization_dir)
        self._ensure_shared_file_link(
            data_dir / Path(MAIN_MEMORY_FILE).name,
            memory_dir / Path(MAIN_MEMORY_FILE).name,
        )
        self._ensure_shared_file_link(
            data_dir / Path(TASK_MEMORY_FILE).name,
            memory_dir / Path(TASK_MEMORY_FILE).name,
        )

    def _migrate_legacy_docker_workspace_layout(self, username: str) -> None:
        """把旧 Docker Web 单项目目录迁移为 projects/default/{project,data,logs}。

        旧布局：
          <user>/project/*
          <user>/data/*
          <user>/logs/*

        新布局：
          <user>/projects/default/project/*
          <user>/projects/default/data/*
          <user>/projects/default/logs/*
          <user>/personal/{personalization,memory,agentskills}
        """
        root = (self.workspace_root / username).resolve()
        projects_root = self._project_container_root(username)
        personal_root = self._personal_root(username)
        legacy_project_root = root / "project"
        default_root = projects_root / "default"
        default_project = default_root / "project"
        default_data = default_root / "data"
        default_logs = default_root / "logs"
        marker = default_root / ".migrated_from_legacy"

        # 兼容上一轮临时结构 users/<user>/project/<id> 与 users/<user>/project/shared。
        if legacy_project_root.exists() and legacy_project_root.is_dir():
            projects_root.mkdir(parents=True, exist_ok=True)

            legacy_shared = legacy_project_root / "shared"
            if legacy_shared.exists() and legacy_shared.is_dir():
                for sub in ("personalization", "memory", "agentskills"):
                    self._move_dir_contents(legacy_shared / sub, personal_root / sub)
                try:
                    shutil.rmtree(legacy_shared, ignore_errors=True)
                except Exception:
                    pass

            for item in list(legacy_project_root.iterdir()):
                if item.name.startswith("."):
                    continue
                if item.name in self.RESERVED_WORKSPACE_IDS - {"default"}:
                    continue
                if self._looks_like_project_workspace_dir(item):
                    dest = projects_root / item.name
                    if not dest.exists():
                        shutil.move(str(item), str(dest))

            # 迁移最老布局 users/<user>/project/* 下的实际文件到 default/project。
            if legacy_project_root.exists() and not marker.exists():
                legacy_children = [
                    child
                    for child in list(legacy_project_root.iterdir())
                    if (
                        not child.name.startswith(".")
                        and not (child.is_dir() and child.name in (self.RESERVED_WORKSPACE_IDS - {"project"}))
                        and not self._looks_like_project_workspace_dir(child)
                    )
                ]
                if legacy_children:
                    default_project.mkdir(parents=True, exist_ok=True)
                    for child in legacy_children:
                        dest = default_project / child.name
                        if not dest.exists():
                            shutil.move(str(child), str(dest))
            try:
                legacy_project_root.rmdir()
            except Exception:
                pass

        # 迁移旧 data/logs。
        legacy_data = root / "data"
        legacy_logs = root / "logs"
        if legacy_data.exists() and legacy_data.is_dir() and legacy_data.resolve() != default_data.resolve():
            if not default_data.exists():
                default_data.parent.mkdir(parents=True, exist_ok=True)
                shutil.move(str(legacy_data), str(default_data))
            else:
                self._move_dir_contents(legacy_data, default_data)
                try:
                    legacy_data.rmdir()
                except Exception:
                    pass
        if legacy_logs.exists() and legacy_logs.is_dir() and legacy_logs.resolve() != default_logs.resolve():
            if not default_logs.exists():
                default_logs.parent.mkdir(parents=True, exist_ok=True)
                shutil.move(str(legacy_logs), str(default_logs))
            else:
                self._move_dir_contents(legacy_logs, default_logs)
                try:
                    legacy_logs.rmdir()
                except Exception:
                    pass
        legacy_agentskills = root / "agentskills"
        if legacy_agentskills.exists() and legacy_agentskills.is_dir():
            self._move_dir_contents(legacy_agentskills, personal_root / "agentskills")
            try:
                legacy_agentskills.rmdir()
            except Exception:
                pass
        try:
            default_root.mkdir(parents=True, exist_ok=True)
            marker.write_text("ok", encoding="utf-8")
        except Exception:
            pass

    def ensure_user_workspace(self, username: str, workspace_id: str = "default") -> UserWorkspace:
        username = self._normalize_username(username)
        workspace_id = self.normalize_workspace_id(workspace_id)

        # host 模式：检查 web 路径是否已有该工作区（保持旧数据可访问）
        if IS_HOST_MODE and self._web_workspace_root:
            web_ws_root = (self._web_workspace_root / username / "projects" / workspace_id).resolve()
            if web_ws_root.exists():
                return self._build_workspace(username, workspace_id, web_ws_root)

        root = (self.workspace_root / username).resolve()
        self._migrate_legacy_docker_workspace_layout(username)
        workspace_root = self._project_container_root(username) / workspace_id
        return self._build_workspace(username, workspace_id, workspace_root)

    def _build_workspace(self, username: str, workspace_id: str, workspace_root: Path) -> UserWorkspace:
        """根据 workspace_root 构建 UserWorkspace 并创建必要目录。"""
        project_path = workspace_root / "project"
        data_dir = workspace_root / "data"
        logs_dir = workspace_root / "logs"
        uploads_dir = project_path / ".astrion" / "user_upload"
        skills_dir = project_path / ".astrion" / "skills"

        for path in [project_path, data_dir, logs_dir, uploads_dir, skills_dir]:
            path.mkdir(parents=True, exist_ok=True)

        # 初始化数据子目录
        (data_dir / "conversations").mkdir(parents=True, exist_ok=True)
        (data_dir / "backups").mkdir(parents=True, exist_ok=True)
        self._ensure_shared_user_state_links(username, data_dir)

        quarantine_root = Path(UPLOAD_QUARANTINE_SUBDIR).expanduser()
        if not quarantine_root.is_absolute():
            quarantine_root = (self.workspace_root.parent / UPLOAD_QUARANTINE_SUBDIR).resolve()
        quarantine_dir = (quarantine_root / username / workspace_id).resolve()
        quarantine_dir.mkdir(parents=True, exist_ok=True)

        workspace = UserWorkspace(
            username=username,
            root=workspace_root,
            project_path=project_path,
            data_dir=data_dir,
            logs_dir=logs_dir,
            uploads_dir=uploads_dir,
            quarantine_dir=quarantine_dir,
        )
        workspace.workspace_id = workspace_id
        return workspace

    def _get_user_default_workspace_id(self, username: str) -> str:
        """返回用户设置的默认项目 ID，如未设置或无效则返回 default。"""
        record = self._users.get(self._normalize_username(username))
        default_id = str(getattr(record, "default_workspace_id", None) or "default").strip()
        if not default_id:
            return "default"
        return default_id

    def list_user_workspaces(self, username: str) -> Dict[str, Dict]:
        username = self._normalize_username(username)
        self._migrate_legacy_docker_workspace_layout(username)
        result: Dict[str, Dict] = {}

        # 收集主路径的工作区
        self._collect_workspaces(result, self._project_container_root(username))

        # host 模式：额外收集 web 路径的工作区
        if IS_HOST_MODE and self._web_workspace_root:
            web_root = (self._web_workspace_root / username / "projects").resolve()
            if web_root.exists():
                self._collect_workspaces(result, web_root)

        default_workspace_id = self._get_user_default_workspace_id(username)

        # 确保 default 始终存在
        if "default" not in result:
            result["default"] = {
                "workspace_id": "default",
                "label": "默认项目",
                "path": "",
                "is_default": default_workspace_id == "default",
                "has_conversations": False,
            }

        # 同步 is_default 标记
        for ws_id in result:
            result[ws_id]["is_default"] = ws_id == default_workspace_id
        return result

    def _collect_workspaces(self, result: Dict[str, Dict], projects_root: Path) -> None:
        """扫描 projects_root 下的工作区，合并到 result（跳过已存在的）。"""
        default_project = projects_root / "default" / "project"
        default_data = projects_root / "default" / "data"
        if "default" not in result:
            if default_project.exists() or default_data.exists():
                result["default"] = {
                    "workspace_id": "default",
                    "label": "默认项目",
                    "path": "",
                    "is_default": True,
                    "has_conversations": (default_data / "conversations").exists(),
                }

        if not projects_root.exists():
            return
        for item in sorted(projects_root.iterdir(), key=lambda p: p.name.lower()):
            if not item.is_dir():
                continue
            ws_id = item.name
            if ws_id in self.RESERVED_WORKSPACE_IDS or ws_id.startswith("."):
                continue
            if ws_id in result:
                continue
            data_dir = item / "data"
            result[ws_id] = {
                "workspace_id": ws_id,
                "label": self._load_workspace_label(item, ws_id),
                "path": "",
                "is_default": False,
                "has_conversations": (data_dir / "conversations").exists(),
            }

    def generate_workspace_id(self, username: str, label: str = "") -> str:
        username = self._normalize_username(username)
        raw = (label or "project").strip().lower()
        slug = re.sub(r"[^a-z0-9]+", "-", raw).strip("-")
        if not slug or slug in self.RESERVED_WORKSPACE_IDS or slug.startswith("."):
            slug = "project"
        slug = slug[:32].strip("-") or "project"
        existing = set(self.list_user_workspaces(username).keys())
        candidate = slug
        idx = 2
        while candidate in existing or candidate in self.RESERVED_WORKSPACE_IDS or candidate.startswith("."):
            suffix = f"-{idx}"
            candidate = f"{slug[: max(1, 40 - len(suffix))]}{suffix}"
            idx += 1
        return candidate

    def create_user_workspace(self, username: str, workspace_id: str = "", label: str = "") -> UserWorkspace:
        ws_id = (
            self.validate_new_workspace_id(workspace_id)
            if workspace_id
            else self.generate_workspace_id(username, label)
        )
        workspace = self.ensure_user_workspace(username, ws_id)
        label_text = (label or "").strip()
        if label_text:
            meta_path = Path(workspace.root) / "project.json"
            try:
                meta_path.write_text(
                    json.dumps({"workspace_id": ws_id, "label": label_text}, ensure_ascii=False, indent=2),
                    encoding="utf-8",
                )
            except Exception:
                pass
        return workspace

    def rename_user_workspace(self, username: str, workspace_id: str, label: str) -> Dict:
        username = self._normalize_username(username)
        ws_id = self.normalize_workspace_id(workspace_id)
        label_text = (label or "").strip()
        if not label_text:
            raise ValueError("项目名称不能为空")
        known = self.list_user_workspaces(username)
        if ws_id not in known:
            raise ValueError("项目不存在")
        workspace = self.ensure_user_workspace(username, ws_id)
        meta_path = Path(workspace.root) / "project.json"
        meta_path.write_text(
            json.dumps({"workspace_id": ws_id, "label": label_text}, ensure_ascii=False, indent=2),
            encoding="utf-8",
        )
        return {
            "workspace_id": ws_id,
            "label": label_text,
            "path": "",
            "is_default": ws_id == "default",
            "has_conversations": (Path(workspace.data_dir) / "conversations").exists(),
        }

    def delete_user_workspace(self, username: str, workspace_id: str) -> None:
        username = self._normalize_username(username)
        ws_id = self.validate_new_workspace_id(workspace_id)
        projects_root = self._project_container_root(username)
        target = (projects_root / ws_id).resolve()
        try:
            target.relative_to(projects_root.resolve())
        except ValueError as exc:
            raise ValueError("项目路径不合法") from exc
        if not target.exists() or not target.is_dir():
            raise ValueError("项目不存在")
        shutil.rmtree(target)

    def set_default_user_workspace(self, username: str, workspace_id: str) -> str:
        """设置指定项目为用户默认项目，返回生效的默认项目 ID。"""
        username = self._normalize_username(username)
        ws_id = self.normalize_workspace_id(workspace_id)
        known = self.list_user_workspaces(username)
        if ws_id not in known:
            raise ValueError("项目不存在")
        record = self._users.get(username)
        if not record:
            raise ValueError("用户不存在")
        record.default_workspace_id = ws_id
        self._save_users()
        return ws_id

    @staticmethod
    def _load_workspace_label(workspace_root: Path, fallback: str) -> str:
        meta_path = workspace_root / "project.json"
        if meta_path.exists():
            try:
                data = json.loads(meta_path.read_text(encoding="utf-8"))
                label = str(data.get("label") or "").strip()
                if label:
                    return label
            except Exception:
                pass
        return fallback

    def list_invite_codes(self):
        return list(self._invites.values())

    def upsert_invite_code(self, code: str, remaining: Optional[int]) -> Dict:
        """新增或更新邀请码。remaining=None 表示不限次数。"""
        invite_code = (code or "").strip()
        if not invite_code:
            raise ValueError("邀请码不能为空。")
        if len(invite_code) > 64:
            raise ValueError("邀请码长度不能超过 64 个字符。")
        if remaining is not None:
            try:
                remaining = int(remaining)
            except (TypeError, ValueError):
                raise ValueError("remaining 必须是整数或 null。")
            if remaining < 0:
                raise ValueError("remaining 不能小于 0。")
        entry = {
            "code": invite_code,
            "remaining": remaining,
        }
        self._invites[invite_code] = entry
        self._save_invite_codes()
        return dict(entry)

    def delete_invite_code(self, code: str) -> bool:
        invite_code = (code or "").strip()
        if not invite_code:
            raise ValueError("邀请码不能为空。")
        existed = invite_code in self._invites
        if existed:
            self._invites.pop(invite_code, None)
            self._save_invite_codes()
        return existed

    def list_users(self) -> Dict[str, UserRecord]:
        """返回当前注册用户的浅拷贝字典，键为用户名。"""
        return dict(self._users)

    def set_tutorial_completed(self, username: str, completed: bool = True) -> Optional[UserRecord]:
        key = (username or "").strip().lower()
        record = self._users.get(key)
        if not record:
            return None
        record.tutorial_completed = bool(completed)
        self._save_users()
        return record

    def reset_password(self, username: str, new_password: str) -> UserRecord:
        """重置用户密码。管理员操作。"""
        key = (username or "").strip().lower()
        password = (new_password or "").strip()
        if not password or len(password) < 8:
            raise ValueError("密码长度至少 8 位。")
        record = self._users.get(key)
        if not record:
            raise ValueError("用户不存在。")
        record.password_hash = generate_password_hash(password)
        self._save_users()
        return record

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------
    def _normalize_username(self, username: str) -> str:
        candidate = (username or "").strip().lower()
        if not candidate or not self.USERNAME_REGEX.match(candidate):
            raise ValueError("用户名需为 3-32 位小写字母、数字、下划线或连字符。")
        return candidate

    def _normalize_email(self, email: str) -> str:
        email = (email or "").strip().lower()
        if "@" not in email or len(email) < 6:
            raise ValueError("邮箱格式不正确。")
        return email

    def _index_user(self, record: UserRecord):
        email = (record.email or '').strip().lower()
        if email:
            self._email_map[email] = record.username

    def _load_users(self):
        if not self.users_file.exists():
            self._save_users()
            # host 模式：即使主文件不存在也尝试加载 web 用户
            self._load_users_from(self.users_file)
            if IS_HOST_MODE and self._web_users_file and self._web_users_file.exists():
                self._load_users_from(self._web_users_file, skip_existing=True)
            return

        self._load_users_from(self.users_file)
        # host 模式：额外合并 web 用户（不覆盖已有）
        if IS_HOST_MODE and self._web_users_file and self._web_users_file.exists():
            self._load_users_from(self._web_users_file, skip_existing=True)

    def _load_users_from(self, file_path: Path, skip_existing: bool = False):
        """从指定文件加载用户，skip_existing 为 True 时跳过已有用户。"""
        if not file_path.exists():
            return
        try:
            migrated = False
            with open(file_path, "r", encoding="utf-8") as f:
                data = json.load(f)
                if isinstance(data, dict):
                    raw_users = data.get("users", {})
                elif isinstance(data, list):
                    raw_users = {item.get("username"): item for item in data if isinstance(item, dict) and item.get("username")}
                else:
                    raw_users = {}
                for username, payload in raw_users.items():
                    if skip_existing and username in self._users:
                        continue
                    record = UserRecord(
                        username=username,
                        email=payload.get("email", ""),
                        password_hash=payload.get("password_hash", ""),
                        created_at=payload.get("created_at", ""),
                        invite_code=payload.get("invite_code"),
                        role=payload.get("role", "user"),
                        tutorial_completed=bool(payload.get("tutorial_completed", False)),
                        default_workspace_id=str(payload.get("default_workspace_id") or "default").strip() or "default",
                    )
                    if "tutorial_completed" not in payload:
                        migrated = True
                    if "default_workspace_id" not in payload:
                        migrated = True
                    self._users[username] = record
                    self._index_user(record)
            if migrated:
                self._save_users()
        except json.JSONDecodeError:
            raise RuntimeError(f"无法解析用户数据文件: {file_path}")

    def _save_users(self):
        payload = {
            "users": {
                username: {
                    "email": record.email,
                    "password_hash": record.password_hash,
                    "created_at": record.created_at,
                    "invite_code": record.invite_code,
                    "role": record.role,
                    "tutorial_completed": bool(record.tutorial_completed),
                    "default_workspace_id": str(record.default_workspace_id or "default").strip() or "default",
                }
                for username, record in self._users.items()
            }
        }
        self.users_file.parent.mkdir(parents=True, exist_ok=True)
        with open(self.users_file, "w", encoding="utf-8") as f:
            json.dump(payload, f, ensure_ascii=False, indent=2)

    def _load_invite_codes(self):
        if not self.invite_codes_file.exists():
            self._save_invite_codes({})
            return

        try:
            with open(self.invite_codes_file, "r", encoding="utf-8") as f:
                data = json.load(f)
                if isinstance(data, dict):
                    codes = data.get("codes", [])
                elif isinstance(data, list):
                    codes = data
                else:
                    codes = []
                self._invites = {item["code"]: item for item in codes if isinstance(item, dict) and "code" in item}
        except json.JSONDecodeError:
            raise RuntimeError(f"无法解析邀请码文件: {self.invite_codes_file}")

    def _save_invite_codes(self, overrides: Optional[Dict[str, Dict]] = None):
        codes = overrides or self._invites
        payload = {"codes": list(codes.values())}
        self.invite_codes_file.parent.mkdir(parents=True, exist_ok=True)
        with open(self.invite_codes_file, "w", encoding="utf-8") as f:
            json.dump(payload, f, ensure_ascii=False, indent=2)

    def _validate_invite_code(self, code: str) -> Dict:
        if not code:
            raise ValueError("邀请码不能为空。")
        entry = self._invites.get(code)
        if not entry:
            raise ValueError("邀请码不存在或已失效。")

        remaining = entry.get("remaining")
        if remaining is not None and remaining <= 0:
            raise ValueError("邀请码已被使用。")
        return entry

    def _consume_invite(self, entry: Dict):
        if entry.get("remaining") is None:
            return
        entry["remaining"] = max(0, entry["remaining"] - 1)
        self._save_invite_codes()

    def _ensure_admin_user(self):
        admin_name = (ADMIN_USERNAME or "").strip().lower()
        if not admin_name or not ADMIN_PASSWORD_HASH:
            return

        if admin_name in self._users:
            return

        record = UserRecord(
            username=admin_name,
            email=f"{admin_name}@local",
            password_hash=ADMIN_PASSWORD_HASH,
            created_at=datetime.utcnow().isoformat(),
            invite_code=None,
            role="admin",
            tutorial_completed=False,
        )
        self._users[admin_name] = record
        self._index_user(record)
        self._save_users()
        self.ensure_user_workspace(admin_name)
