# -*- coding: utf-8 -*-
"""数据访问层（Repository）：SQLAlchemy/SQLite 实现。

每个 Repository 对应一张表，只做行级操作，不掺入 HTTP/路由逻辑。
方法签名与原先 JSON 实现保持一致，路由与业务逻辑不变。
"""
from __future__ import annotations

import json
import secrets
from datetime import datetime, timezone

from sqlalchemy import delete, select
from sqlalchemy.orm import Session

from . import config
from .models import (
    Agent,
    AuditLog,
    Bid,
    ContentItem,
    Contract,
    Dispute,
    Escrow,
    FinanceRecord,
    InvestmentIntent,
    InvestorPreference,
    Message,
    Notification,
    OpcProfile,
    OpcTask,
    Organization,
    OrganizationMember,
    Permission,
    PortalDashboard,
    PortalPage,
    Rating,
    Region,
    Roadshow,
    RoadshowRegistration,
    Role,
    RolePermission,
    ServiceProvider,
    ServiceReferral,
    SessionToken,
    SubsidyApplication,
    SystemConfig,
    Task,
    TrainingEnrollment,
    User,
    UserIdentity,
)
from .security import hash_password, verify_password

USER_TABLE = "users"
TOKEN_TABLE = "tokens"
AGENTS_TABLE = "agents"

# 内置智能体身份（唯一来源在服务端；本地据此同步 workspace）
DEFAULT_AGENT_ID = "pine_agents_official_001"
QA_AGENT_ID = "pine_agents_official_002"
AGENT_SEED = (
    {
        "id": DEFAULT_AGENT_ID,
        "name": "小园",
        "description": "默认助手，处理和园区、创业、政策等相关工作",
        "language": "zh",
        "model_name": "",
        "template_type": "default",
        "deletable": False,
        "use_fixed_soul": True,
    },
    {
        "id": QA_AGENT_ID,
        "name": "问答助手",
        "description": (
            "内置 PineAgents 设置问答助手，本地配置在 `PINEAGENTS_WORKING_DIR` 下，"
            "并提供文档。建议在回答前阅读文件；在此工作区外使用绝对路径编写代码。"
        ),
        "language": "zh",
        "model_name": "",
        "template_type": "qa",
        "deletable": True,
        "use_fixed_soul": False,
    },
)

PROFILE_FIELDS = (
    "username",
    "nickname",
    "account",
    "company",
    "room",
    "avatar",
    "company_avatar",
)


# ---------------------------------------------------------------------------
# 记录工具
# ---------------------------------------------------------------------------
def utcnow_iso() -> str:
    """当前 UTC 时间，ISO 8601 字符串（如 ``2026-08-02T12:00:00+00:00``）。"""
    return datetime.now(timezone.utc).isoformat()


def new_id(prefix: str) -> str:
    """生成记录主键，如 ``u_<hex>`` / ``sess_<hex>``。"""
    return f"{prefix}_{secrets.token_hex(12)}"


# 业务角色 → 端口映射（与 rbac_admin._port_for_role 保持一致）
_PORT_FOR_ROLE = {
    "opc_member": "opc", "carrier": "carrier", "enterprise": "enterprise",
    "provider": "provider", "government": "government", "operator": "operator",
    "investor": "investor", "developer": "developer",
}


def is_expired(expires_at: str) -> bool:
    """判断 ``expires_at``（ISO 字符串）是否已过期。"""
    try:
        return datetime.fromisoformat(expires_at) < datetime.now(timezone.utc)
    except (ValueError, TypeError):
        return True


def _iso_in(seconds: int) -> str:
    """返回当前时刻往后 ``seconds`` 秒的 ISO 时间戳。"""
    return datetime.fromtimestamp(
        datetime.now(timezone.utc).timestamp() + seconds,
        tz=timezone.utc,
    ).isoformat()


def _user_to_dict(u: User) -> dict:
    return {
        "id": u.id,
        "username": u.username,
        "password_hash": u.password_hash,
        "password_salt": u.password_salt,
        "nickname": u.nickname,
        "account": u.account,
        "company": u.company,
        "room": u.room,
        "avatar": u.avatar,
        "company_avatar": u.company_avatar,
        "role": u.role,
        "sub_role": u.sub_role,
        "org_id": u.org_id,
        "region_id": u.region_id,
        "status": u.status,
        "token_version": u.token_version,
        "created_at": u.created_at,
        "updated_at": u.updated_at,
    }


# ---------------------------------------------------------------------------
# 用户表
# ---------------------------------------------------------------------------
class UserRepository:
    def __init__(self, session: Session):
        self.session = session

    def has_users(self) -> bool:
        return self.session.scalar(select(User.id).limit(1)) is not None

    def get_by_username(self, username: str) -> dict | None:
        u = self.session.scalar(
            select(User).where(User.username.ilike(username.strip())),
        )
        return _user_to_dict(u) if u else None

    def get_by_id(self, user_id: str) -> dict | None:
        u = self.session.get(User, user_id)
        return _user_to_dict(u) if u else None

    def list(self) -> list[dict]:
        return [_user_to_dict(u) for u in self.session.scalars(select(User).order_by(User.created_at))]

    def create(
        self,
        username: str,
        password: str,
        *,
        nickname: str = "",
        account: str = "",
        company: str = "",
        room: str = "",
        avatar: str = "",
        company_avatar: str = "",
        role: str = "opc_member",
        sub_role: str | None = None,
        org_id: str | None = None,
        region_id: str | None = None,
    ) -> dict:
        digest, salt = hash_password(password)
        now = utcnow_iso()
        u = User(
            id=new_id("u"),
            username=username.strip(),
            password_hash=digest,
            password_salt=salt,
            nickname=nickname,
            account=account,
            company=company,
            room=room,
            avatar=avatar,
            company_avatar=company_avatar,
            role=role,
            sub_role=sub_role,
            org_id=org_id,
            region_id=region_id,
            status="active",
            token_version=0,
            created_at=now,
            updated_at=now,
        )
        self.session.add(u)
        self.session.commit()
        return _user_to_dict(u)

    def update_profile(self, user_id: str, fields: dict) -> dict | None:
        u = self.session.get(User, user_id)
        if u is None:
            return None
        allowed = {k: fields[k] for k in fields if k in PROFILE_FIELDS}
        if not allowed:
            return None
        for k, v in allowed.items():
            setattr(u, k, v)
        u.updated_at = utcnow_iso()
        self.session.commit()
        return _user_to_dict(u)

    def update_credentials(
        self,
        user_id: str,
        new_username: str | None,
        new_password: str | None,
    ) -> dict | None:
        u = self.session.get(User, user_id)
        if u is None:
            return None
        if new_username is not None:
            u.username = new_username.strip()
        if new_password is not None:
            digest, salt = hash_password(new_password)
            u.password_hash = digest
            u.password_salt = salt
        u.updated_at = utcnow_iso()
        self.session.commit()
        return _user_to_dict(u)

    def verify_password(self, user: dict, password: str) -> bool:
        return verify_password(
            password,
            user.get("password_hash", ""),
            user.get("password_salt", ""),
        )

    def set_role(
        self,
        user_id: str,
        role: str,
        sub_role: str | None,
        org_id: str | None,
        region_id: str | None,
    ) -> dict | None:
        u = self.session.get(User, user_id)
        if u is None:
            return None
        # 同步该用户"主角色"身份（角色改变需对已绑定身份会话生效）
        new_port = _PORT_FOR_ROLE.get(role, "opc")
        for ident in self.session.scalars(
            select(UserIdentity).where(UserIdentity.user_id == user_id)
        ):
            if ident.role == u.role:
                ident.role = role
                ident.sub_role = sub_role
                ident.port = new_port
                ident.org_id = org_id
                ident.region_id = region_id
                ident.updated_at = utcnow_iso()
        u.role = role
        u.sub_role = sub_role
        u.org_id = org_id
        u.region_id = region_id
        u.token_version += 1  # 使既有令牌失效，角色变更即时生效
        u.updated_at = utcnow_iso()
        self.session.commit()
        return _user_to_dict(u)

    def set_status(self, user_id: str, status: str) -> dict | None:
        u = self.session.get(User, user_id)
        if u is None:
            return None
        u.status = status
        u.updated_at = utcnow_iso()
        self.session.commit()
        return _user_to_dict(u)

    def bump_token_version(self, user_id: str) -> int:
        u = self.session.get(User, user_id)
        if u is None:
            return 0
        u.token_version += 1
        u.updated_at = utcnow_iso()
        self.session.commit()
        return u.token_version

    def to_profile(self, user: dict) -> dict:
        """把用户记录裁剪成对外暴露的资料结构（不含任何密码字段）。"""
        profile = {field: user.get(field, "") for field in PROFILE_FIELDS}
        profile["role"] = user.get("role", "opc_member")
        profile["sub_role"] = user.get("sub_role")
        profile["org_id"] = user.get("org_id")
        profile["region_id"] = user.get("region_id")
        profile["permissions"] = user.get("permissions", [])
        profile["scope_region_ids"] = user.get("scope_region_ids", [])
        profile["identity_id"] = user.get("identity_id")
        profile["port"] = user.get("port")
        profile["identity_name"] = user.get("identity_name", "")
        return profile


# ---------------------------------------------------------------------------
# 令牌表（JWT + sessions 撤销台账）
# ---------------------------------------------------------------------------
class TokenRepository:
    def __init__(self, session: Session):
        self.session = session

    def create(
        self,
        user: dict,
        *,
        permissions: list[str],
        scope_region_ids: list[str],
        scope_level: str | None,
        expiry_seconds: int,
        identity_id: str | None = None,
    ) -> dict:
        """签发 JWT 并登记 sessions 行，返回 {token, id, user_id, ...}。"""
        from .jwt import create_access_token

        token, jti, expires_at = create_access_token(
            user,
            permissions=permissions,
            scope_region_ids=scope_region_ids,
            scope_level=scope_level,
            expiry_seconds=expiry_seconds,
            identity_id=identity_id,
        )
        now = utcnow_iso()
        rec = SessionToken(
            id=jti,
            jti=jti,
            token=token,
            user_id=user["id"],
            username=user.get("username", ""),
            created_at=now,
            expires_at=expires_at,
            revoked=False,
        )
        self.session.add(rec)
        self.session.commit()
        return {"id": jti, "token": token, "user_id": user["id"]}

    def get(self, token: str) -> dict | None:
        """按 JWT 解码得到的 jti 查会话行。"""
        from .jwt import decode_access_token

        payload = decode_access_token(token)
        if not payload:
            return None
        rec = self.session.scalar(
            select(SessionToken).where(SessionToken.jti == payload.get("jti")),
        )
        if rec is None:
            return None
        return {
            "id": rec.id,
            "jti": rec.jti,
            "token": rec.token,
            "user_id": rec.user_id,
            "username": rec.username,
            "expires_at": rec.expires_at,
            "revoked": rec.revoked,
        }

    def session_valid(self, jti: str, user_id: str, ver: int) -> bool:
        """JWT 声称的会话与用户版本是否仍有效。"""
        rec = self.session.scalar(
            select(SessionToken).where(SessionToken.jti == jti),
        )
        if rec is None or rec.revoked or rec.user_id != user_id:
            return False
        if is_expired(rec.expires_at):
            return False
        u = self.session.get(User, user_id)
        return u is not None and u.token_version == ver and u.status == "active"

    def get_valid_user_id(self, token: str) -> str | None:
        """JWT 有效（验签、会话未吊销、版本一致、用户启用）时返回 user_id。"""
        from .jwt import decode_access_token

        payload = decode_access_token(token)
        if not payload:
            return None
        user_id = payload.get("sub")
        if not self.session_valid(payload.get("jti", ""), user_id, payload.get("ver", 0)):
            return None
        return user_id

    def revoke(self, token: str) -> bool:
        rec = self.get(token)
        if rec is None:
            return False
        row = self.session.get(SessionToken, rec["id"])
        if row is None:
            return False
        row.revoked = True
        self.session.commit()
        return True

    def revoke_all(self, user_id: str | None = None) -> int:
        """吊销会话（指定用户或全部），并递增其 token_version 使旧 JWT 失效。"""
        if user_id is not None:
            affected = self.session.execute(
                delete(SessionToken).where(SessionToken.user_id == user_id)
            ).rowcount
            u = self.session.get(User, user_id)
            if u is not None:
                u.token_version += 1
            self.session.commit()
            return affected
        affected = self.session.execute(delete(SessionToken)).rowcount
        for u in self.session.scalars(select(User)):
            u.token_version += 1
        self.session.commit()
        return affected


# ---------------------------------------------------------------------------
# 角色 / 权限
# ---------------------------------------------------------------------------
class RoleRepository:
    def __init__(self, session: Session):
        self.session = session

    def list_roles(self) -> list[dict]:
        matrix = self.role_matrix()
        return [
            {
                "id": r.id, "name": r.name, "scope_type": r.scope_type,
                "description": r.description, "permissions": matrix.get(r.id, []),
            }
            for r in self.session.scalars(select(Role).order_by(Role.scope_type, Role.id))
        ]

    def list_permissions(self) -> list[dict]:
        return [
            {"id": p.id, "name": p.name, "category": p.category, "module": p.module}
            for p in self.session.scalars(select(Permission).order_by(Permission.module, Permission.id))
        ]

    def permissions_for(self, role: str, sub_role: str | None) -> list[str]:
        """返回该用户（业务角色 + 可选子角色）的全部权限码。

        合并 role 与 ``role|sub_role`` 两个角色的权限。
        """
        ids = [role]
        if sub_role:
            ids.append(f"{role}|{sub_role}")
        return sorted(
            {
                pid
                for pid, in self.session.execute(
                    select(RolePermission.permission_id).where(
                        RolePermission.role_id.in_(ids)
                    )
                )
            }
        )

    def role_matrix(self) -> dict[str, list[str]]:
        rows = self.session.execute(
            select(RolePermission.role_id, RolePermission.permission_id)
        ).all()
        matrix: dict[str, list[str]] = {}
        for role_id, perm_id in rows:
            matrix.setdefault(role_id, []).append(perm_id)
        return matrix

    def role_exists(self, role_id: str) -> bool:
        return self.session.get(Role, role_id) is not None

    def set_role_permissions(self, role_id: str, permissions: list[str]) -> list[str]:
        """重设某角色的权限集合（角色权限配置，仅超管）。"""
        self.session.execute(
            delete(RolePermission).where(RolePermission.role_id == role_id)
        )
        for pid in permissions:
            if self.session.get(Permission, pid) is not None:
                self.session.add(RolePermission(role_id=role_id, permission_id=pid))
        self.session.commit()
        return self.permissions_for_role(role_id)

    def permissions_for_role(self, role_id: str) -> list[str]:
        if "|" in role_id:
            role, sub = role_id.split("|", 1)
            return self.permissions_for(role, sub)
        return self.permissions_for(role_id, None)


# ---------------------------------------------------------------------------
# 区域（数据范围层级）
# ---------------------------------------------------------------------------
class RegionRepository:
    def __init__(self, session: Session):
        self.session = session

    def get(self, region_id: str) -> dict | None:
        r = self.session.get(Region, region_id)
        return {"id": r.id, "name": r.name, "level": r.level, "parent_id": r.parent_id} if r else None

    def all(self) -> list[dict]:
        return [
            {"id": r.id, "name": r.name, "level": r.level, "parent_id": r.parent_id}
            for r in self.session.scalars(select(Region))
        ]

    def level(self, region_id: str) -> str | None:
        r = self.session.get(Region, region_id)
        return r.level if r else None

    def _descendants(self, region_id: str) -> set[str]:
        """收集某区域的全部后代 id（含自身）。"""
        result = {region_id}
        rows = self.session.execute(
            select(Region.id, Region.parent_id)
        ).all()
        children: dict[str, list[str]] = {}
        for rid, parent in rows:
            if parent:
                children.setdefault(parent, []).append(rid)
        stack = list(children.get(region_id, []))
        while stack:
            cur = stack.pop()
            if cur in result:
                continue
            result.add(cur)
            stack.extend(children.get(cur, []))
        return result

    def visible_region_ids(self, region_id: str | None) -> list[str]:
        """数据范围：本域 + 全部后代（上级可看下级）。无区域则返回空。"""
        if not region_id:
            return []
        return sorted(self._descendants(region_id))


# ---------------------------------------------------------------------------
# 组织（企业 / 载体 / 服务商）
# ---------------------------------------------------------------------------
class OrgRepository:
    def __init__(self, session: Session):
        self.session = session

    def _to_dict(self, o: Organization) -> dict:
        return {
            "id": o.id,
            "name": o.name,
            "type": o.type,
            "region_id": o.region_id,
            "parent_id": o.parent_id,
            "status": o.status,
            "created_at": o.created_at,
        }

    def get(self, org_id: str) -> dict | None:
        o = self.session.get(Organization, org_id)
        return self._to_dict(o) if o else None

    def all(self) -> list[dict]:
        return [self._to_dict(o) for o in self.session.scalars(select(Organization))]

    def list_by_region(self, region_ids: list[str]) -> list[dict]:
        if not region_ids:
            return []
        return [
            self._to_dict(o)
            for o in self.session.scalars(
                select(Organization).where(Organization.region_id.in_(region_ids))
            )
        ]

    def list_by_parent(self, parent_id: str) -> list[dict]:
        return [
            self._to_dict(o)
            for o in self.session.scalars(
                select(Organization).where(Organization.parent_id == parent_id)
            )
        ]


# ---------------------------------------------------------------------------
# 审计日志
# ---------------------------------------------------------------------------
class AuditRepository:
    def __init__(self, session: Session):
        self.session = session

    def add(self, *, action: str, resource: str, resource_id: str = "",
            detail: str = "", ip: str = "", user_id: str | None = None) -> None:
        self.session.add(
            AuditLog(
                id=new_id("aud"),
                user_id=user_id,
                action=action,
                resource=resource,
                resource_id=resource_id,
                detail=detail,
                ip=ip,
                created_at=utcnow_iso(),
            )
        )
        self.session.commit()

    def list(self, limit: int = 100, offset: int = 0) -> list[dict]:
        rows = self.session.scalars(
            select(AuditLog).order_by(AuditLog.created_at.desc()).limit(limit).offset(offset)
        )
        return [
            {
                "id": a.id,
                "user_id": a.user_id,
                "action": a.action,
                "resource": a.resource,
                "resource_id": a.resource_id,
                "detail": a.detail,
                "ip": a.ip,
                "created_at": a.created_at,
            }
            for a in rows
        ]


# ---------------------------------------------------------------------------
# 智能体表（身份记录；按用户隔离）
# ---------------------------------------------------------------------------
class AgentRepository:
    def __init__(self, session: Session):
        self.session = session

    def _to_dict(self, a: Agent) -> dict:
        return {
            "id": a.id,
            "user_id": a.user_id,
            "port": a.port,
            "name": a.name,
            "description": a.description,
            "language": a.language,
            "model_name": a.model_name,
            "deletable": a.deletable,
            "use_fixed_soul": a.use_fixed_soul,
            "created_at": a.created_at,
            "updated_at": a.updated_at,
        }

    def get_by_user(self, user_id: str, port: str | None = None) -> list[dict]:
        if not port:
            return []
        stmt = select(Agent).where(Agent.user_id == user_id, Agent.port == port)
        rows = self.session.scalars(stmt.order_by(Agent.created_at))
        return [self._to_dict(a) for a in rows]

    def get(self, agent_id: str, user_id: str, port: str | None = None) -> dict | None:
        if not port:
            return None
        stmt = select(Agent).where(Agent.id == agent_id, Agent.user_id == user_id, Agent.port == port)
        a = self.session.scalar(stmt)
        return self._to_dict(a) if a else None

    def count_by_user(self, user_id: str, port: str | None = None) -> int:
        if not port:
            return 0
        stmt = select(Agent.id).where(Agent.user_id == user_id, Agent.port == port)
        return len(list(self.session.scalars(stmt)))

    def create(
        self,
        user_id: str,
        name: str,
        *,
        description: str = "",
        language: str = "zh",
        model_name: str = "",
        agent_id: str | None = None,
        port: str = "opc",
    ) -> dict:
        now = utcnow_iso()
        a = Agent(
            id=agent_id or new_id("agent"),
            user_id=user_id,
            port=port,
            name=name.strip(),
            description=description,
            language=language,
            model_name=model_name,
            deletable=True,
            use_fixed_soul=False,
            created_at=now,
            updated_at=now,
        )
        self.session.add(a)
        self.session.commit()
        return self._to_dict(a)

    def update(self, agent_id: str, user_id: str, fields: dict, port: str | None = None) -> dict | None:
        if not port:
            return None
        stmt = select(Agent).where(Agent.id == agent_id, Agent.user_id == user_id, Agent.port == port)
        a = self.session.scalar(stmt)
        if a is None:
            return None
        allowed = {
            k: fields[k]
            for k in fields
            if k in ("name", "description", "language", "model_name", "use_fixed_soul")
        }
        if not allowed:
            return None
        for k, v in allowed.items():
            setattr(a, k, v)
        a.updated_at = utcnow_iso()
        self.session.commit()
        return self._to_dict(a)

    def delete(self, agent_id: str, user_id: str, port: str | None = None) -> bool:
        if not port:
            return False
        stmt = select(Agent).where(Agent.id == agent_id, Agent.user_id == user_id, Agent.port == port)
        a = self.session.scalar(stmt)
        if a is None:
            return False
        self.session.delete(a)
        self.session.commit()
        return True


# ---------------------------------------------------------------------------
# 任务
# ---------------------------------------------------------------------------
class TaskRepository:
    def __init__(self, session: Session):
        self.session = session

    def _to_dict(self, t: Task) -> dict:
        return {
            "id": t.id, "title": t.title, "category": t.category,
            "sub_category": t.sub_category, "description": t.description,
            "mode": t.mode, "budget_min": t.budget_min, "budget_max": t.budget_max,
            "deadline": t.deadline, "status": t.status,
            "publisher_org_id": t.publisher_org_id, "publisher_name": t.publisher_name,
            "created_at": t.created_at, "updated_at": t.updated_at,
        }

    def list(self, status: str | None = None) -> list[dict]:
        q = select(Task).order_by(Task.created_at.desc())
        if status:
            q = q.where(Task.status == status)
        return [self._to_dict(t) for t in self.session.scalars(q)]

    def get(self, task_id: str) -> dict | None:
        t = self.session.get(Task, task_id)
        return self._to_dict(t) if t else None

    def create(self, fields: dict) -> dict:
        now = utcnow_iso()
        t = Task(id=new_id("task"), title=fields.get("title", ""),
                 category=fields.get("category", ""), sub_category=fields.get("sub_category", ""),
                 description=fields.get("description", ""), mode=fields.get("mode", "grab"),
                 budget_min=fields.get("budget_min", 0), budget_max=fields.get("budget_max", 0),
                 deadline=fields.get("deadline", ""), status=fields.get("status", "draft"),
                 publisher_org_id=fields.get("publisher_org_id"),
                 publisher_name=fields.get("publisher_name", ""),
                 created_at=now, updated_at=now)
        self.session.add(t)
        self.session.commit()
        return self._to_dict(t)

    def set_status(self, task_id: str, status: str) -> dict | None:
        t = self.session.get(Task, task_id)
        if t is None:
            return None
        t.status = status
        t.updated_at = utcnow_iso()
        self.session.commit()
        return self._to_dict(t)


# ---------------------------------------------------------------------------
# 服务商
# ---------------------------------------------------------------------------
class ProviderRepository:
    def __init__(self, session: Session):
        self.session = session

    def _to_dict(self, p: ServiceProvider) -> dict:
        return {
            "id": p.id, "name": p.name, "category": p.category,
            "org_id": p.org_id, "level": p.level, "rating": p.rating,
            "order_count": p.order_count, "status": p.status,
            "contact": p.contact, "created_at": p.created_at, "updated_at": p.updated_at,
        }

    def list(self, status: str | None = None) -> list[dict]:
        q = select(ServiceProvider).order_by(ServiceProvider.created_at.desc())
        if status:
            q = q.where(ServiceProvider.status == status)
        return [self._to_dict(p) for p in self.session.scalars(q)]

    def create(self, fields: dict) -> dict:
        now = utcnow_iso()
        p = ServiceProvider(id=new_id("prov"), name=fields.get("name", ""),
                            category=fields.get("category", ""), org_id=fields.get("org_id"),
                            level=fields.get("level", "certified"), rating=fields.get("rating", 5.0),
                            order_count=fields.get("order_count", 0),
                            status=fields.get("status", "pending"),
                            contact=fields.get("contact", ""), created_at=now, updated_at=now)
        self.session.add(p)
        self.session.commit()
        return self._to_dict(p)

    def update(self, provider_id: str, fields: dict) -> dict | None:
        p = self.session.get(ServiceProvider, provider_id)
        if p is None:
            return None
        for k in ("name", "category", "level", "status", "contact"):
            if k in fields:
                setattr(p, k, fields[k])
        if "rating" in fields:
            p.rating = fields["rating"]
        p.updated_at = utcnow_iso()
        self.session.commit()
        return self._to_dict(p)


# ---------------------------------------------------------------------------
# 内容
# ---------------------------------------------------------------------------
class ContentRepository:
    def __init__(self, session: Session):
        self.session = session

    def _to_dict(self, c: ContentItem) -> dict:
        return {
            "id": c.id, "type": c.type, "title": c.title, "summary": c.summary,
            "body": c.body, "publisher_id": c.publisher_id, "status": c.status,
            "created_at": c.created_at, "updated_at": c.updated_at,
        }

    def list(self, ctype: str | None = None, status: str | None = None) -> list[dict]:
        q = select(ContentItem).order_by(ContentItem.created_at.desc())
        if ctype:
            q = q.where(ContentItem.type == ctype)
        if status:
            q = q.where(ContentItem.status == status)
        return [self._to_dict(c) for c in self.session.scalars(q)]

    def create(self, fields: dict) -> dict:
        now = utcnow_iso()
        c = ContentItem(id=new_id("cont"), type=fields.get("type", "news"),
                        title=fields.get("title", ""), summary=fields.get("summary", ""),
                        body=fields.get("body", ""), publisher_id=fields.get("publisher_id"),
                        status=fields.get("status", "draft"), created_at=now, updated_at=now)
        self.session.add(c)
        self.session.commit()
        return self._to_dict(c)

    def set_status(self, content_id: str, status: str) -> dict | None:
        c = self.session.get(ContentItem, content_id)
        if c is None:
            return None
        c.status = status
        c.updated_at = utcnow_iso()
        self.session.commit()
        return self._to_dict(c)


# ---------------------------------------------------------------------------
# 系统配置
# ---------------------------------------------------------------------------
class ConfigRepository:
    def __init__(self, session: Session):
        self.session = session

    def all(self) -> list[dict]:
        return [
            {"key": c.key, "value": c.value, "description": c.description,
             "updated_at": c.updated_at}
            for c in self.session.scalars(select(SystemConfig).order_by(SystemConfig.key))
        ]

    def get(self, key: str) -> str | None:
        c = self.session.get(SystemConfig, key)
        return c.value if c else None

    def set(self, key: str, value: str, description: str = "") -> dict:
        c = self.session.get(SystemConfig, key)
        if c is None:
            c = SystemConfig(key=key, value=value, description=description, updated_at=utcnow_iso())
            self.session.add(c)
        else:
            c.value = value
            if description:
                c.description = description
            c.updated_at = utcnow_iso()
        self.session.commit()
        return {"key": c.key, "value": c.value, "description": c.description, "updated_at": c.updated_at}


# ---------------------------------------------------------------------------
# OPC 超级个体（工作台聚合数据）
# ---------------------------------------------------------------------------
class OpcProfileRepository:
    """OPC 档案（信用评分等）。"""

    def __init__(self, session: Session):
        self.session = session

    def get(self, user_id: str) -> dict | None:
        row = self.session.get(OpcProfile, user_id)
        return self._to_dict(row) if row else None

    def upsert(self, user_id: str, credit_score: int = 80) -> dict:
        row = self.session.get(OpcProfile, user_id)
        if row is None:
            row = OpcProfile(
                user_id=user_id, credit_score=credit_score,
                created_at=utcnow_iso(), updated_at=utcnow_iso(),
            )
            self.session.add(row)
        else:
            row.credit_score = credit_score
            row.updated_at = utcnow_iso()
        self.session.commit()
        return self._to_dict(row)

    @staticmethod
    def _to_dict(p: OpcProfile) -> dict:
        return {
            "user_id": p.user_id, "credit_score": p.credit_score,
            "updated_at": p.updated_at,
        }


class FinanceRepository:
    """OPC 财务流水（收入/支出）与月度聚合。"""

    def __init__(self, session: Session):
        self.session = session

    def list_by_user(self, user_id: str, category: str | None = None) -> list[dict]:
        stmt = select(FinanceRecord).where(FinanceRecord.user_id == user_id)
        if category:
            stmt = stmt.where(FinanceRecord.category == category)
        rows = self.session.scalars(stmt.order_by(FinanceRecord.date.desc())).all()
        return [self._to_dict(f) for f in rows]

    def total_income(self, user_id: str) -> int:
        return sum(
            f.amount
            for f in self.session.scalars(
                select(FinanceRecord).where(
                    FinanceRecord.user_id == user_id,
                    FinanceRecord.category == "income",
                )
            )
        )

    def monthly_income(self, user_id: str, limit: int = 6) -> list[dict]:
        """按 YYYY-MM 汇总收入，取最近 N 个月（升序返回）。"""
        agg: dict[str, int] = {}
        for f in self.session.scalars(
            select(FinanceRecord).where(
                FinanceRecord.user_id == user_id,
                FinanceRecord.category == "income",
            )
        ):
            month = (f.date or "")[:7]
            if not month:
                continue
            agg[month] = agg.get(month, 0) + f.amount
        months = sorted(agg.keys())[-limit:]
        return [{"month": m, "amount": agg[m]} for m in months]

    @staticmethod
    def _to_dict(f: FinanceRecord) -> dict:
        return {
            "id": f.id, "user_id": f.user_id, "category": f.category,
            "amount": f.amount, "date": f.date, "note": f.note,
            "created_at": f.created_at,
        }


class MessageRepository:
    """OPC 站内消息。"""

    def __init__(self, session: Session):
        self.session = session

    def recent(self, user_id: str, limit: int = 4) -> list[dict]:
        rows = self.session.scalars(
            select(Message)
            .where(Message.user_id == user_id)
            .order_by(Message.created_at.desc())
            .limit(limit)
        ).all()
        return [self._to_dict(m) for m in rows]

    def list_by_user(self, user_id: str) -> list[dict]:
        rows = self.session.scalars(
            select(Message)
            .where(Message.user_id == user_id)
            .order_by(Message.created_at.desc())
        ).all()
        return [self._to_dict(m) for m in rows]

    def unread_count(self, user_id: str) -> int:
        return len(
            self.session.scalars(
                select(Message).where(Message.user_id == user_id, Message.read.is_(False))
            ).all()
        )

    @staticmethod
    def _to_dict(m: Message) -> dict:
        return {
            "id": m.id, "user_id": m.user_id, "msg_type": m.msg_type,
            "title": m.title, "content": m.content, "read": m.read,
            "created_at": m.created_at,
        }


class OpcTaskRepository:
    """OPC 个人任务看板。"""

    def __init__(self, session: Session):
        self.session = session

    def list_by_user(self, user_id: str) -> list[dict]:
        rows = self.session.scalars(
            select(OpcTask)
            .where(OpcTask.user_id == user_id)
            .order_by(OpcTask.created_at.desc())
        ).all()
        return [self._to_dict(t) for t in rows]

    def counts(self, user_id: str) -> dict:
        in_progress = 0
        completed = 0
        for row in self.session.scalars(
            select(OpcTask).where(OpcTask.user_id == user_id)
        ):
            if row.status == "completed":
                completed += 1
            elif row.status in ("in_progress", "urgent"):
                in_progress += 1
        return {"in_progress": in_progress, "completed": completed}

    @staticmethod
    def _to_dict(t: OpcTask) -> dict:
        return {
            "id": t.id, "user_id": t.user_id, "title": t.title, "status": t.status,
            "budget": t.budget, "deadline": t.deadline, "progress": t.progress,
            "created_at": t.created_at, "updated_at": t.updated_at,
        }


# ---------------------------------------------------------------------------
# 账号↔端口身份（多身份绑定）
# ---------------------------------------------------------------------------
class IdentityRepository:
    """一个账号可绑定的多个端口身份（登录后选择其一进入）。"""

    def __init__(self, session: Session):
        self.session = session

    def _to_dict(self, i: UserIdentity) -> dict:
        org = self.session.get(Organization, i.org_id) if i.org_id else None
        region = self.session.get(Region, i.region_id) if i.region_id else None
        return {
            "id": i.id,
            "user_id": i.user_id,
            "port": i.port,
            "role": i.role,
            "sub_role": i.sub_role,
            "name": i.name,
            "org_id": i.org_id,
            "region_id": i.region_id,
            "org_name": org.name if org else None,
            "region_name": region.name if region else None,
            "status": i.status,
            "created_at": i.created_at,
            "updated_at": i.updated_at,
        }

    def get(self, identity_id: str) -> dict | None:
        row = self.session.get(UserIdentity, identity_id)
        return self._to_dict(row) if row else None

    def get_for_user(self, identity_id: str, user_id: str) -> dict | None:
        row = self.session.scalar(
            select(UserIdentity).where(
                UserIdentity.id == identity_id,
                UserIdentity.user_id == user_id,
            )
        )
        return self._to_dict(row) if row else None

    def list_for_user(self, user_id: str, active_only: bool = True) -> list[dict]:
        stmt = select(UserIdentity).where(UserIdentity.user_id == user_id)
        if active_only:
            stmt = stmt.where(UserIdentity.status == "active")
        rows = self.session.scalars(stmt.order_by(UserIdentity.port)).all()
        return [self._to_dict(i) for i in rows]

    def create(
        self,
        user_id: str,
        *,
        port: str,
        role: str | None = None,
        sub_role: str | None = None,
        name: str = "",
        org_id: str | None = None,
        region_id: str | None = None,
    ) -> dict:
        now = utcnow_iso()
        row = UserIdentity(
            id=new_id("ident"),
            user_id=user_id,
            port=port,
            role=role or port,
            sub_role=sub_role,
            name=name,
            org_id=org_id,
            region_id=region_id,
            status="active",
            created_at=now,
            updated_at=now,
        )
        self.session.add(row)
        self.session.commit()
        return self._to_dict(row)

    def set_status(self, identity_id: str, status: str) -> dict | None:
        row = self.session.get(UserIdentity, identity_id)
        if row is None:
            return None
        row.status = status
        row.updated_at = utcnow_iso()
        self.session.commit()
        return self._to_dict(row)


# ---------------------------------------------------------------------------
# 端口工作台（按端口 JSON 载荷）
# ---------------------------------------------------------------------------
class PortalDashboardRepository:
    def __init__(self, session: Session):
        self.session = session

    def get(self, port: str) -> dict | None:
        row = self.session.get(PortalDashboard, port)
        if row is None:
            return None
        try:
            payload = json.loads(row.payload)
        except (ValueError, TypeError):
            return {}
        payload["port"] = row.port
        payload["updated_at"] = row.updated_at
        return payload

    def set(self, port: str, payload: dict) -> dict:
        row = self.session.get(PortalDashboard, port)
        now = utcnow_iso()
        data = json.dumps(payload, ensure_ascii=False)
        if row is None:
            row = PortalDashboard(port=port, payload=data, updated_at=now)
            self.session.add(row)
        else:
            row.payload = data
            row.updated_at = now
        self.session.commit()
        return self.get(port)


class PortalPageRepository:
    """端口子页面数据（按 端口+页面 存 JSON 载荷）。"""

    def __init__(self, session: Session):
        self.session = session

    def get(self, port: str, page: str) -> dict | None:
        row = self.session.get(PortalPage, (port, page))
        if row is None:
            return None
        try:
            return json.loads(row.payload)
        except (ValueError, TypeError):
            return {}

    def set(self, port: str, page: str, payload: dict) -> dict:
        row = self.session.get(PortalPage, (port, page))
        now = utcnow_iso()
        data = json.dumps(payload, ensure_ascii=False)
        if row is None:
            row = PortalPage(port=port, page=page, payload=data, updated_at=now)
            self.session.add(row)
        else:
            row.payload = data
            row.updated_at = now
        self.session.commit()
        return self.get(port, page)


# ---------------------------------------------------------------------------
# 投资人端（偏好 / 路演 / 报名 / 意向）
# ---------------------------------------------------------------------------
class InvestorPreferenceRepository:
    def __init__(self, session: Session):
        self.session = session

    def get(self, user_id: str) -> dict | None:
        row = self.session.get(InvestorPreference, user_id)
        if row is None:
            return None
        return {
            "user_id": row.user_id,
            "industries": json.loads(row.industries or "[]"),
            "stage": row.stage,
            "amount_min": row.amount_min,
            "amount_max": row.amount_max,
            "region_id": row.region_id,
            "updated_at": row.updated_at,
        }

    def upsert(self, user_id: str, fields: dict) -> dict:
        row = self.session.get(InvestorPreference, user_id)
        now = utcnow_iso()
        if row is None:
            row = InvestorPreference(user_id=user_id, updated_at=now)
            self.session.add(row)
        if "industries" in fields:
            row.industries = json.dumps(fields["industries"], ensure_ascii=False)
        if "stage" in fields:
            row.stage = fields["stage"] or ""
        if "amount_min" in fields:
            row.amount_min = fields["amount_min"] or 0
        if "amount_max" in fields:
            row.amount_max = fields["amount_max"] or 0
        if "region_id" in fields:
            row.region_id = fields["region_id"]
        row.updated_at = now
        self.session.commit()
        return self.get(user_id)


class RoadshowRepository:
    def __init__(self, session: Session):
        self.session = session

    def _to_dict(self, r: Roadshow) -> dict:
        region = self.session.get(Region, r.region_id) if r.region_id else None
        return {
            "id": r.id, "publisher_id": r.publisher_id, "publisher_role": r.publisher_role,
            "title": r.title, "summary": r.summary, "activity_type": r.activity_type,
            "scope_type": r.scope_type, "region_id": r.region_id,
            "region_name": region.name if region else None,
            "start_at": r.start_at, "end_at": r.end_at, "register_deadline": r.register_deadline,
            "quota": r.quota, "venue": r.venue, "live_url": r.live_url,
            "status": r.status, "need_review": r.need_review,
            "review_comment": r.review_comment, "created_at": r.created_at, "updated_at": r.updated_at,
        }

    def list(self, status: str | None = None) -> list[dict]:
        stmt = select(Roadshow).order_by(Roadshow.created_at.desc())
        if status:
            stmt = stmt.where(Roadshow.status == status)
        return [self._to_dict(r) for r in self.session.scalars(stmt)]

    def get(self, roadshow_id: str) -> dict | None:
        row = self.session.get(Roadshow, roadshow_id)
        return self._to_dict(row) if row else None

    def create(self, fields: dict) -> dict:
        now = utcnow_iso()
        row = Roadshow(
            id=new_id("rs"), publisher_id=fields.get("publisher_id", ""),
            publisher_role=fields.get("publisher_role", "investor"),
            title=fields.get("title", ""), summary=fields.get("summary", ""),
            activity_type=fields.get("activity_type", "online"),
            scope_type=fields.get("scope_type", "all"),
            region_id=fields.get("region_id"),
            start_at=fields.get("start_at", ""), end_at=fields.get("end_at", ""),
            register_deadline=fields.get("register_deadline", ""),
            quota=fields.get("quota", 0), venue=fields.get("venue", ""),
            live_url=fields.get("live_url", ""),
            status=fields.get("status", "draft"),
            need_review=fields.get("need_review", True),
            created_at=now, updated_at=now,
        )
        self.session.add(row)
        self.session.commit()
        return self._to_dict(row)

    def set_status(self, roadshow_id: str, status: str, review_comment: str = "") -> dict | None:
        row = self.session.get(Roadshow, roadshow_id)
        if row is None:
            return None
        row.status = status
        if review_comment:
            row.review_comment = review_comment
        row.updated_at = utcnow_iso()
        self.session.commit()
        return self._to_dict(row)


class RoadshowRegistrationRepository:
    def __init__(self, session: Session):
        self.session = session

    def create(self, roadshow_id: str, user_id: str, role: str = "investor", note: str = "") -> dict:
        row = RoadshowRegistration(
            id=new_id("rsreg"), roadshow_id=roadshow_id, user_id=user_id,
            role=role, status="applying", note=note, created_at=utcnow_iso(),
        )
        self.session.add(row)
        self.session.commit()
        return {"id": row.id, "roadshow_id": row.roadshow_id, "user_id": row.user_id,
                "role": row.role, "status": row.status, "note": row.note}


class InvestmentIntentRepository:
    def __init__(self, session: Session):
        self.session = session

    def create(self, investor_id: str, project_id: str, project_name: str, message: str = "") -> dict:
        row = InvestmentIntent(
            id=new_id("intent"), investor_id=investor_id, project_id=project_id,
            project_name=project_name, status="interested", message=message,
            created_at=utcnow_iso(), updated_at=utcnow_iso(),
        )
        self.session.add(row)
        self.session.commit()
        return {"id": row.id, "investor_id": row.investor_id, "project_id": row.project_id,
                "project_name": row.project_name, "status": row.status, "message": row.message}

    def list_for(self, investor_id: str) -> list[dict]:
        rows = self.session.scalars(
            select(InvestmentIntent).where(InvestmentIntent.investor_id == investor_id)
            .order_by(InvestmentIntent.created_at.desc())
        ).all()
        return [{"id": r.id, "project_id": r.project_id, "project_name": r.project_name,
                 "status": r.status, "message": r.message, "created_at": r.created_at} for r in rows]


class BidRepository:
    """任务竞标。"""

    def __init__(self, session: Session):
        self.session = session

    def list_for_task(self, task_id: str) -> list[dict]:
        rows = self.session.scalars(
            select(Bid).where(Bid.task_id == task_id).order_by(Bid.created_at.desc())
        ).all()
        return [self._to_dict(r) for r in rows]

    def get(self, bid_id: str) -> dict | None:
        row = self.session.get(Bid, bid_id)
        return self._to_dict(row) if row else None

    def create(self, task_id: str, opc_id: str, opc_name: str, quote: int, plan: str = "") -> dict:
        row = Bid(
            id=new_id("bid"), task_id=task_id, opc_id=opc_id, opc_name=opc_name,
            quote=quote, plan=plan, status="submitted",
            created_at=utcnow_iso(), updated_at=utcnow_iso(),
        )
        self.session.add(row)
        self.session.commit()
        return self._to_dict(row)

    def set_status(self, bid_id: str, status: str) -> dict | None:
        row = self.session.get(Bid, bid_id)
        if row is None:
            return None
        row.status = status
        row.updated_at = utcnow_iso()
        self.session.commit()
        return self._to_dict(row)

    @staticmethod
    def _to_dict(b: Bid) -> dict:
        return {"id": b.id, "task_id": b.task_id, "opc_id": b.opc_id, "opc_name": b.opc_name,
                "quote": b.quote, "plan": b.plan, "status": b.status,
                "created_at": b.created_at, "updated_at": b.updated_at}


class SubsidyRepository:
    """补贴申报（政务三级审批）。"""

    def __init__(self, session: Session):
        self.session = session

    def list(self, region_ids: list[str] | None = None) -> list[dict]:
        rows = self.session.scalars(select(SubsidyApplication).order_by(SubsidyApplication.created_at.desc()))
        out = []
        for r in rows:
            if region_ids and r.region_id and r.region_id not in region_ids:
                continue
            out.append(self._to_dict(r))
        return out

    def get(self, aid: str) -> dict | None:
        row = self.session.get(SubsidyApplication, aid)
        return self._to_dict(row) if row else None

    def create(self, fields: dict) -> dict:
        now = utcnow_iso()
        row = SubsidyApplication(
            id=new_id("sub"), opc_id=fields.get("opc_id", ""), opc_name=fields.get("opc_name", ""),
            title=fields.get("title", ""), amount=fields.get("amount", 0),
            region_id=fields.get("region_id"), status="applying",
            created_at=now, updated_at=now,
        )
        self.session.add(row)
        self.session.commit()
        return self._to_dict(row)

    def set_status(self, aid: str, status: str, comment: str = "") -> dict | None:
        row = self.session.get(SubsidyApplication, aid)
        if row is None:
            return None
        row.status = status
        if comment:
            row.comment = comment
        row.updated_at = utcnow_iso()
        self.session.commit()
        return self._to_dict(row)

    @staticmethod
    def _to_dict(s: SubsidyApplication) -> dict:
        return {"id": s.id, "opc_id": s.opc_id, "opc_name": s.opc_name, "title": s.title,
                "amount": s.amount, "region_id": s.region_id, "status": s.status,
                "comment": s.comment, "created_at": s.created_at}


class ServiceReferralRepository:
    """载体-服务商引荐。"""

    def __init__(self, session: Session):
        self.session = session

    def list_for(self, carrier_id: str | None = None) -> list[dict]:
        stmt = select(ServiceReferral).order_by(ServiceReferral.created_at.desc())
        if carrier_id:
            stmt = stmt.where(ServiceReferral.carrier_id == carrier_id)
        return [self._to_dict(r) for r in self.session.scalars(stmt)]

    def create(self, fields: dict) -> dict:
        row = ServiceReferral(
            id=new_id("ref"), carrier_id=fields.get("carrier_id", ""),
            provider_id=fields.get("provider_id", ""), provider_name=fields.get("provider_name", ""),
            opc_id=fields.get("opc_id", ""), opc_name=fields.get("opc_name", ""),
            status="referred", created_at=utcnow_iso(), updated_at=utcnow_iso(),
        )
        self.session.add(row)
        self.session.commit()
        return self._to_dict(row)

    @staticmethod
    def _to_dict(r: ServiceReferral) -> dict:
        return {"id": r.id, "carrier_id": r.carrier_id, "provider_id": r.provider_id,
                "provider_name": r.provider_name, "opc_id": r.opc_id, "opc_name": r.opc_name,
                "status": r.status, "created_at": r.created_at}


class TrainingEnrollmentRepository:
    """投融资培训报名。"""

    def __init__(self, session: Session):
        self.session = session

    def list_for(self, user_id: str) -> list[dict]:
        rows = self.session.scalars(
            select(TrainingEnrollment).where(TrainingEnrollment.user_id == user_id)
        ).all()
        return [{"id": r.id, "training_id": r.training_id, "training_name": r.training_name,
                 "status": r.status, "created_at": r.created_at} for r in rows]

    def create(self, user_id: str, training_id: str, training_name: str) -> dict:
        row = TrainingEnrollment(
            id=new_id("treg"), user_id=user_id, training_id=training_id,
            training_name=training_name, status="enrolled", created_at=utcnow_iso(),
        )
        self.session.add(row)
        self.session.commit()
        return {"id": row.id, "training_id": row.training_id, "training_name": row.training_name,
                "status": row.status}


class EscrowRepository:
    """任务资金托管/结算。"""

    def __init__(self, session: Session):
        self.session = session

    def list(self, status: str | None = None) -> list[dict]:
        stmt = select(Escrow).order_by(Escrow.created_at.desc())
        if status:
            stmt = stmt.where(Escrow.status == status)
        return [self._to_dict(r) for r in self.session.scalars(stmt)]

    def get(self, escrow_id: str) -> dict | None:
        row = self.session.get(Escrow, escrow_id)
        return self._to_dict(row) if row else None

    def create(self, task_id: str, task_title: str, amount: int, commission: int = 0) -> dict:
        row = Escrow(id=new_id("esc"), task_id=task_id, task_title=task_title,
                     amount=amount, commission=commission, status="deposited",
                     created_at=utcnow_iso(), updated_at=utcnow_iso())
        self.session.add(row)
        self.session.commit()
        return self._to_dict(row)

    def set_status(self, escrow_id: str, status: str) -> dict | None:
        row = self.session.get(Escrow, escrow_id)
        if row is None:
            return None
        row.status = status
        row.updated_at = utcnow_iso()
        self.session.commit()
        return self._to_dict(row)

    @staticmethod
    def _to_dict(e: Escrow) -> dict:
        return {"id": e.id, "task_id": e.task_id, "task_title": e.task_title, "amount": e.amount,
                "commission": e.commission, "status": e.status, "created_at": e.created_at}


class ContractRepository:
    def __init__(self, session: Session):
        self.session = session

    def get_for_task(self, task_id: str) -> dict | None:
        row = self.session.scalar(select(Contract).where(Contract.task_id == task_id))
        return self._to_dict(row) if row else None

    def create(self, task_id: str, task_title: str, enterprise_id: str, opc_id: str) -> dict:
        row = Contract(id=new_id("ct"), task_id=task_id, task_title=task_title,
                       enterprise_id=enterprise_id, opc_id=opc_id, status="signed",
                       content=f"任务《{task_title}》电子合同，双方已签署，资金由平台托管。",
                       created_at=utcnow_iso())
        self.session.add(row)
        self.session.commit()
        return self._to_dict(row)

    @staticmethod
    def _to_dict(c: Contract) -> dict:
        return {"id": c.id, "task_id": c.task_id, "task_title": c.task_title,
                "enterprise_id": c.enterprise_id, "opc_id": c.opc_id, "status": c.status,
                "content": c.content, "created_at": c.created_at}


class DisputeRepository:
    def __init__(self, session: Session):
        self.session = session

    def list(self, status: str | None = None) -> list[dict]:
        stmt = select(Dispute).order_by(Dispute.created_at.desc())
        if status:
            stmt = stmt.where(Dispute.status == status)
        return [self._to_dict(r) for r in self.session.scalars(stmt)]

    def create(self, task_id: str, task_title: str, initiator: str, reason: str) -> dict:
        row = Dispute(id=new_id("disp"), task_id=task_id, task_title=task_title,
                      initiator=initiator, reason=reason, status="opened", created_at=utcnow_iso())
        self.session.add(row)
        self.session.commit()
        return self._to_dict(row)

    def set_status(self, dispute_id: str, status: str, resolution: str = "") -> dict | None:
        row = self.session.get(Dispute, dispute_id)
        if row is None:
            return None
        row.status = status
        if resolution:
            row.resolution = resolution
        self.session.commit()
        return self._to_dict(row)

    @staticmethod
    def _to_dict(d: Dispute) -> dict:
        return {"id": d.id, "task_id": d.task_id, "task_title": d.task_title,
                "initiator": d.initiator, "reason": d.reason, "status": d.status,
                "resolution": d.resolution, "created_at": d.created_at}


class RatingRepository:
    def __init__(self, session: Session):
        self.session = session

    def avg_for(self, user_id: str) -> float:
        rows = self.session.scalars(select(Rating).where(Rating.to_id == user_id)).all()
        return round(sum(r.score for r in rows) / len(rows), 1) if rows else 5.0

    def create(self, task_id: str, from_id: str, to_id: str, score: int, comment: str = "") -> dict:
        row = Rating(id=new_id("rate"), task_id=task_id, from_id=from_id, to_id=to_id,
                     score=max(1, min(5, score)), comment=comment, created_at=utcnow_iso())
        self.session.add(row)
        self.session.commit()
        return {"id": row.id, "score": row.score, "comment": row.comment}


class NotificationRepository:
    def __init__(self, session: Session):
        self.session = session

    def list_for(self, user_id: str, limit: int = 50) -> list[dict]:
        rows = self.session.scalars(
            select(Notification).where(Notification.user_id == user_id)
            .order_by(Notification.created_at.desc()).limit(limit)
        ).all()
        return [{"id": r.id, "type": r.type, "title": r.title, "content": r.content,
                 "read": r.read, "created_at": r.created_at} for r in rows]

    def unread(self, user_id: str) -> int:
        return len(self.session.scalars(
            select(Notification).where(Notification.user_id == user_id, Notification.read.is_(False))
        ).all())

    def create(self, user_id: str, type: str, title: str, content: str) -> dict:
        row = Notification(id=new_id("ntf"), user_id=user_id, type=type, title=title,
                           content=content, read=False, created_at=utcnow_iso())
        self.session.add(row)
        self.session.commit()
        return {"id": row.id, "title": row.title}

    def mark_read(self, user_id: str) -> int:
        rows = self.session.scalars(select(Notification).where(Notification.user_id == user_id)).all()
        for r in rows:
            r.read = True
        self.session.commit()
        return len(rows)


# ---------------------------------------------------------------------------
# 机构成员（机构主账号 + 机构内子账号）
# ---------------------------------------------------------------------------
class OrganizationMemberRepository:
    def __init__(self, session: Session):
        self.session = session

    def list_for_org(self, org_id: str) -> list[dict]:
        rows = self.session.scalars(
            select(OrganizationMember).where(OrganizationMember.org_id == org_id)
        ).all()
        out = []
        for r in rows:
            u = self.session.get(User, r.user_id)
            out.append({"org_id": r.org_id, "user_id": r.user_id,
                        "username": u.username if u else r.user_id,
                        "nickname": u.nickname if u else "",
                        "role": r.role, "is_admin": r.is_admin, "status": r.status})
        return out

    def is_member(self, org_id: str, user_id: str) -> bool:
        return self.session.get(OrganizationMember, (org_id, user_id)) is not None

    def is_admin(self, org_id: str, user_id: str) -> bool:
        row = self.session.get(OrganizationMember, (org_id, user_id))
        return bool(row and row.is_admin)

    def add_member(self, org_id: str, user_id: str, role: str = "member", is_admin: bool = False) -> dict:
        row = self.session.get(OrganizationMember, (org_id, user_id))
        if row is None:
            row = OrganizationMember(org_id=org_id, user_id=user_id, role=role,
                                     is_admin=is_admin, status="active", joined_at=utcnow_iso())
            self.session.add(row)
        else:
            row.role = role
            row.is_admin = is_admin
        self.session.commit()
        return {"org_id": org_id, "user_id": user_id, "role": role, "is_admin": is_admin}


# ---------------------------------------------------------------------------
# 统计
# ---------------------------------------------------------------------------
class StatsRepository:
    """运营端/政务端数据总览（基于现有表聚合）。"""

    def __init__(self, session: Session):
        self.session = session

    def overview(self, region_ids: list[str] | None = None) -> dict:
        users = self.session.scalars(select(User)).all()
        if region_ids is not None:
            scope = set(region_ids)
            users = [u for u in users if not u.region_id or u.region_id in scope]
        tasks = self.session.scalars(select(Task)).all()
        providers = self.session.scalars(select(ServiceProvider)).all()
        content = self.session.scalars(select(ContentItem)).all()
        active_users = [u for u in users if u.status == "active"]
        return {
            "user_count": len(users),
            "active_user_count": len(active_users),
            "task_count": len(tasks),
            "published_task_count": len([t for t in tasks if t.status == "published"]),
            "provider_count": len(providers),
            "active_provider_count": len([p for p in providers if p.status == "active"]),
            "content_count": len(content),
            "published_content_count": len([c for c in content if c.status == "published"]),
        }


# ---------------------------------------------------------------------------
# 数据库门面
# ---------------------------------------------------------------------------
class Database:
    """持有全部 Repository，统一访问入口。"""

    def __init__(self, db_url: str | None = None, session: Session | None = None):
        if session is None:
            from sqlalchemy.orm import sessionmaker

            from .db import make_engine

            url = db_url or config.DATABASE_URL
            self._engine = make_engine(url)
            self._session_factory = sessionmaker(
                bind=self._engine, autoflush=False, expire_on_commit=False,
            )
            self._owns_session = True
        else:
            self._engine = None
            self._session_factory = lambda: session  # type: ignore[assignment]
            self._owns_session = False

        self.session = self._session_factory()
        self.users = UserRepository(self.session)
        self.tokens = TokenRepository(self.session)
        self.agents = AgentRepository(self.session)
        self.roles = RoleRepository(self.session)
        self.orgs = OrgRepository(self.session)
        self.regions = RegionRepository(self.session)
        self.audit = AuditRepository(self.session)
        self.tasks = TaskRepository(self.session)
        self.providers = ProviderRepository(self.session)
        self.content = ContentRepository(self.session)
        self.config = ConfigRepository(self.session)
        self.identities = IdentityRepository(self.session)
        self.portal_dashboards = PortalDashboardRepository(self.session)
        self.portal_pages = PortalPageRepository(self.session)
        self.opc_profiles = OpcProfileRepository(self.session)
        self.finance = FinanceRepository(self.session)
        self.messages = MessageRepository(self.session)
        self.opc_tasks = OpcTaskRepository(self.session)
        self.investor_prefs = InvestorPreferenceRepository(self.session)
        self.roadshows = RoadshowRepository(self.session)
        self.roadshow_regs = RoadshowRegistrationRepository(self.session)
        self.intents = InvestmentIntentRepository(self.session)
        self.bids = BidRepository(self.session)
        self.subsidies = SubsidyRepository(self.session)
        self.referrals = ServiceReferralRepository(self.session)
        self.training_enrolls = TrainingEnrollmentRepository(self.session)
        self.escrows = EscrowRepository(self.session)
        self.contracts = ContractRepository(self.session)
        self.disputes = DisputeRepository(self.session)
        self.ratings = RatingRepository(self.session)
        self.notifications = NotificationRepository(self.session)
        self.org_members = OrganizationMemberRepository(self.session)
        self.stats = StatsRepository(self.session)

    def initialize(self) -> None:
        """建表（本实例的 engine）+ 幂等种子（roles 空时才写）。"""
        from .seed import seed_data

        if self._engine is not None:
            from .db import create_all

            create_all(self._engine)
        seed_data(self.session)

    def close(self) -> None:
        if self._owns_session:
            self.session.close()
