# -*- coding: utf-8 -*-
"""园区租户（多租户）数据层 —— 唯一总库（SQLAlchemy async，config.DATABASE_URL）。

表：park_tenants / park_companies / park_kb_docs / park_screens（ORM 见 app/infrastructure/models.py）。
建表/种子由 alembic + scripts/db/seed.py 在非运行态完成；本层仅读写。运行时不建表不灌种子。
sim_engine 需同步 tick，故另提供同步只读访问器 get_tenant_data_sync。
"""
from __future__ import annotations

import hashlib
import json
import secrets
import time
import uuid

from sqlalchemy import delete, select, update
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker

from ..config import DATABASE_URL
from ..infrastructure.db import make_async_engine
from ..infrastructure.models import ParkCompany, ParkKbDoc, ParkScreen, ParkTenant
from .sim_engine import DEFAULT_DATA, refresh_engine

_DEFAULT_TENANT_ID = "T001"

# 单一异步引擎（进程内共享，避免每操作重建）；建表/种子不走这里
_ASYNC_ENGINE = None


def _get_session() -> AsyncSession:
    global _ASYNC_ENGINE
    if _ASYNC_ENGINE is None:
        _ASYNC_ENGINE = make_async_engine(DATABASE_URL)
    return async_sessionmaker(bind=_ASYNC_ENGINE, expire_on_commit=False)()


def _hash_password(password: str, salt: str | None = None) -> tuple[str, str]:
    salt = salt or secrets.token_hex(16)
    h = hashlib.pbkdf2_hmac("sha256", password.encode(), salt.encode(), 100_000).hex()
    return salt, h


def _verify_password(password: str, salt: str, expected: str) -> bool:
    return _hash_password(password, salt)[1] == expected


def _now() -> str:
    return time.strftime("%Y-%m-%d %H:%M:%S")


def _new_id(prefix: str) -> str:
    return f"{prefix}{int(uuid.uuid4().int % 1000000000):09d}"


# ---------------- 同步只读（sim_engine 同步 tick 线程用） ----------------

def get_tenant_data_sync(tenant_id: str) -> dict:
    """同步读取该租户 data + companies（sim_engine 同步 tick，无事件循环时用）。"""
    from sqlalchemy import create_engine, text
    url = DATABASE_URL.replace("+aiosqlite", "")
    engine = create_engine(url, future=True)
    try:
        with engine.connect() as conn:
            row = conn.execute(text("SELECT data_json FROM park_tenants WHERE id=:id"), {"id": tenant_id}).fetchone()
            data = json.loads(row[0] if row and row[0] else "{}")
            comps = [_rd(r) for r in conn.execute(
                text("SELECT * FROM park_companies WHERE tenant_id=:tid ORDER BY created_at DESC"), {"tid": tenant_id})]
        if comps:
            data = dict(data)
            data["companies"] = comps
        return data or dict(DEFAULT_DATA)
    finally:
        engine.dispose()


def _sync_conn():
    from sqlalchemy import create_engine
    return create_engine(DATABASE_URL.replace("+aiosqlite", ""), future=True)


def list_tenants_sync() -> list[dict]:
    from sqlalchemy import text
    engine = _sync_conn()
    try:
        with engine.connect() as conn:
            rows = conn.execute(text("SELECT * FROM park_tenants ORDER BY created_at DESC")).fetchall()
            return [_to_tenant(r) for r in rows]
    finally:
        engine.dispose()


def list_companies_sync(tenant_id: str) -> list[dict]:
    from sqlalchemy import text
    engine = _sync_conn()
    try:
        with engine.connect() as conn:
            return [_rd(r) for r in conn.execute(
                text("SELECT * FROM park_companies WHERE tenant_id=:tid ORDER BY created_at DESC"), {"tid": tenant_id})]
    finally:
        engine.dispose()


def get_agent_sync(tenant_id: str) -> dict:
    from sqlalchemy import text
    engine = _sync_conn()
    try:
        with engine.connect() as conn:
            row = conn.execute(text("SELECT agent_json FROM park_tenants WHERE id=:id"), {"id": tenant_id}).fetchone()
            return json.loads(row[0]) if row and row[0] else {}
    finally:
        engine.dispose()


# ---------------- 映射 ----------------

def _val(row, key):
    if isinstance(row, dict):
        return row.get(key)
    if hasattr(row, key):
        return getattr(row, key)
    return row[key]


def _rd(row) -> dict:
    """sqlalchemy Row → dict（兼容 Row/RowMapping）。"""
    if isinstance(row, dict):
        return row
    return dict(row._mapping) if hasattr(row, "_mapping") else {k: row[k] for k in row.keys()}


def _to_tenant(row) -> dict:
    return {"id": _val(row, "id"), "name": _val(row, "name"),
            "intro": json.loads(_val(row, "intro_json") or "[]"),
            "auth": {"username": _val(row, "username"), "salt": _val(row, "salt") or "", "password_hash": _val(row, "password_hash") or ""},
            "admin_username": _val(row, "admin_username") or "", "status": _val(row, "status") or "active",
            "data": json.loads(_val(row, "data_json") or "{}"), "agent": json.loads(_val(row, "agent_json") or "{}")}


def _co(r) -> dict:
    return {"id": r.id, "name": r.name, "zone": r.zone, "room": r.room, "industry": r.industry, "bio": r.bio,
            "founder": r.founder, "status": r.status, "employees": r.employees, "created_at": r.created_at}


def _kb(r) -> dict:
    return {"id": r.id, "grp": r.grp, "title": r.title, "content_md": r.content_md, "created_at": r.created_at}


def _sc(r) -> dict:
    return {"id": r.id, "tenant_id": r.tenant_id, "device_id": r.device_id, "name": r.name, "role": r.role,
            "location": r.location, "status": r.status, "code": r.code, "created_at": r.created_at}


def _summary(tid: str, name: str, intro: list, username: str, admin_username: str = "", status: str = "active") -> dict:
    return {"id": tid, "name": name, "intro": intro, "username": username, "admin_username": admin_username, "status": status}


# ---------------- 租户 ----------------

async def list_tenants() -> list[dict]:
    async with _get_session() as s:
        rows = (await s.execute(select(ParkTenant).order_by(ParkTenant.created_at.desc()))).scalars().all()
        return [_to_tenant(r) for r in rows]


async def get_tenant(tenant_id: str) -> dict | None:
    async with _get_session() as s:
        row = await s.get(ParkTenant, tenant_id)
        return _to_tenant(row) if row else None


async def get_tenant_data(tenant_id: str) -> dict:
    data = (await get_tenant(tenant_id) or {}).get("data", {})
    comps = await list_companies(tenant_id)
    if comps:
        data = dict(data)
        data["companies"] = comps
    return data or dict(DEFAULT_DATA)


async def create_tenant(name: str, intro: list[str], username: str = "", password: str = "", tenant_id: str | None = None) -> dict:
    tid = tenant_id or _new_id("T")
    data = dict(DEFAULT_DATA)
    async with _get_session() as s:
        s.add(ParkTenant(id=tid, name=name, intro_json=json.dumps(intro or ["", ""], ensure_ascii=False),
                         username=username, salt="", password_hash="", admin_username="", status="active",
                         data_json=json.dumps(data, ensure_ascii=False), agent_json="{}", created_at=_now()))
        await s.commit()
        await _append_companies(s, tid, DEFAULT_DATA["companies"])
        await s.commit()
    refresh_engine(tid, get_tenant_data_sync(tid))
    return _summary(tid, name, intro, username)


async def _append_companies(s, tenant_id: str, companies: list) -> None:
    for c in companies:
        nm = c["name"] if isinstance(c, dict) else c
        exists = (await s.execute(select(ParkCompany).where(ParkCompany.tenant_id == tenant_id, ParkCompany.name == nm).limit(1))).scalars().first()
        if exists:
            continue
        s.add(ParkCompany(id=_new_id("PC"), tenant_id=tenant_id, name=nm,
             zone=c.get("zone", "") if isinstance(c, dict) else "", room=c.get("room", "") if isinstance(c, dict) else "",
             industry=c.get("industry", "") if isinstance(c, dict) else "", bio=c.get("bio", "") if isinstance(c, dict) else "",
             founder=c.get("founder", "") if isinstance(c, dict) else "", status="active", employees=None, created_at=_now()))


async def bind_admin(tenant_id: str, username: str) -> bool:
    async with _get_session() as s:
        r = await s.execute(update(ParkTenant).where(ParkTenant.id == tenant_id).values(admin_username=username.strip()))
        await s.commit()
        return r.rowcount > 0


async def unbind_admin(tenant_id: str) -> bool:
    return await bind_admin(tenant_id, "")


async def find_by_admin(username: str) -> dict | None:
    async with _get_session() as s:
        row = (await s.execute(select(ParkTenant).where(ParkTenant.admin_username == username.strip()).limit(1))).scalars().first()
        return _to_tenant(row) if row else None


async def update_tenant(tenant_id: str, patch: dict) -> dict | None:
    async with _get_session() as s:
        row = await s.get(ParkTenant, tenant_id)
        if row is None:
            return None
        if patch.get("name") is not None:
            row.name = patch["name"]
        if patch.get("intro") is not None:
            row.intro_json = json.dumps(patch["intro"], ensure_ascii=False)
        if patch.get("data") is not None:
            row.data_json = json.dumps(patch["data"], ensure_ascii=False)
        if patch.get("agent") is not None:
            row.agent_json = json.dumps(patch["agent"], ensure_ascii=False)
        if patch.get("username") is not None:
            row.username = patch["username"]
        if patch.get("password"):
            salt, h = _hash_password(patch["password"])
            row.salt, row.password_hash = salt, h
        await s.commit()
        await s.refresh(row)
        if patch.get("data"):
            refresh_engine(tenant_id, json.loads(row.data_json))
    return _summary(tenant_id, row.name, json.loads(row.intro_json or "[]"), row.username, row.admin_username or "", row.status or "active")


async def delete_tenant(tenant_id: str) -> bool:
    async with _get_session() as s:
        row = await s.get(ParkTenant, tenant_id)
        if row is None:
            return False
        await s.delete(row)  # 级联删除 companies/kb/screens（FK CASCADE）
        await s.commit()
    from .sim_engine import _engines
    _engines.pop(tenant_id, None)
    return True


async def set_status(tenant_id: str, status: str) -> bool:
    if status not in ("active", "disabled"):
        return False
    async with _get_session() as s:
        r = await s.execute(update(ParkTenant).where(ParkTenant.id == tenant_id).values(status=status))
        await s.commit()
        return r.rowcount > 0


async def ensure_default_tenant() -> str:
    t = await get_tenant(_DEFAULT_TENANT_ID)
    if t is None:
        await create_tenant("昆明市大学生创业园", ["云南省首家政府主办大学生创业孵化园区", "空间 + 孵化 + 融资 + 政策 + AI 赋能 + 综合服务"], "admin", tenant_id=_DEFAULT_TENANT_ID)
    elif not await list_companies(_DEFAULT_TENANT_ID):
        async with _get_session() as s:
            await _append_companies(s, _DEFAULT_TENANT_ID, DEFAULT_DATA["companies"])
            await s.commit()
    return _DEFAULT_TENANT_ID


# ---------------- 入驻企业 ----------------

async def list_companies(tenant_id: str) -> list[dict]:
    async with _get_session() as s:
        rows = (await s.execute(select(ParkCompany).where(ParkCompany.tenant_id == tenant_id).order_by(ParkCompany.created_at.desc()))).scalars().all()
        return [_co(r) for r in rows]


async def create_company(tenant_id: str, payload: dict) -> dict:
    row = ParkCompany(id=_new_id("PC"), tenant_id=tenant_id, name=payload.get("name", ""),
        zone=payload.get("zone", ""), room=payload.get("room", ""), industry=payload.get("industry", ""),
        bio=payload.get("bio", ""), founder=payload.get("founder", ""),
        status=payload.get("status", "applying"), employees=payload.get("employees"), created_at=_now())
    async with _get_session() as s:
        s.add(row)
        await s.commit()
    refresh_engine(tenant_id, get_tenant_data_sync(tenant_id))
    return _co(row)


async def update_company(tenant_id: str, cid: str, payload: dict) -> dict | None:
    fields = {k: v for k, v in payload.items() if v is not None and k in ("name", "zone", "room", "industry", "bio", "founder", "status", "employees")}
    if not fields:
        return None
    async with _get_session() as s:
        row = (await s.execute(select(ParkCompany).where(ParkCompany.id == cid, ParkCompany.tenant_id == tenant_id))).scalars().first()
        if row is None:
            return None
        for k, v in fields.items():
            setattr(row, k, v)
        await s.commit()
        await s.refresh(row)
    refresh_engine(tenant_id, get_tenant_data_sync(tenant_id))
    return _co(row)


async def delete_company(tenant_id: str, cid: str) -> bool:
    async with _get_session() as s:
        r = await s.execute(delete(ParkCompany).where(ParkCompany.id == cid, ParkCompany.tenant_id == tenant_id))
        await s.commit()
        ok = r.rowcount > 0
    refresh_engine(tenant_id, get_tenant_data_sync(tenant_id))
    return ok


# ---------------- 智能体 ----------------

async def get_agent(tenant_id: str) -> dict:
    return (await get_tenant(tenant_id) or {}).get("agent", {})


async def update_agent(tenant_id: str, payload: dict) -> dict:
    async with _get_session() as s:
        row = await s.get(ParkTenant, tenant_id)
        agent = json.loads(row.agent_json or "{}")
        for k, v in payload.items():
            if v is not None:
                agent[k] = v
        row.agent_json = json.dumps(agent, ensure_ascii=False)
        await s.commit()
    return agent


# ---------------- 知识库 ----------------

async def kb_docs(tenant_id: str) -> list[dict]:
    async with _get_session() as s:
        rows = (await s.execute(select(ParkKbDoc).where(ParkKbDoc.tenant_id == tenant_id).order_by(ParkKbDoc.created_at.desc()))).scalars().all()
        return [_kb(r) for r in rows]


async def create_kb_doc(tenant_id: str, payload: dict) -> dict:
    row = ParkKbDoc(id=_new_id("KB"), tenant_id=tenant_id, grp=payload.get("group", "general"),
                    title=payload.get("title", ""), content_md=payload.get("content_md", ""), created_at=_now())
    async with _get_session() as s:
        s.add(row)
        await s.commit()
    return _kb(row)


async def update_kb_doc(tenant_id: str, did: str, payload: dict) -> dict | None:
    async with _get_session() as s:
        row = (await s.execute(select(ParkKbDoc).where(ParkKbDoc.id == did, ParkKbDoc.tenant_id == tenant_id))).scalars().first()
        if row is None:
            return None
        for k, v in payload.items():
            if v is not None and k in ("grp", "title", "content_md"):
                setattr(row, k, v)
        await s.commit()
        await s.refresh(row)
    return _kb(row)


async def delete_kb_doc(tenant_id: str, did: str) -> bool:
    async with _get_session() as s:
        r = await s.execute(delete(ParkKbDoc).where(ParkKbDoc.id == did, ParkKbDoc.tenant_id == tenant_id))
        await s.commit()
        return r.rowcount > 0


async def get_kb_doc(tenant_id: str, did: str) -> dict | None:
    async with _get_session() as s:
        row = (await s.execute(select(ParkKbDoc).where(ParkKbDoc.id == did, ParkKbDoc.tenant_id == tenant_id))).scalars().first()
        return _kb(row) if row else None


# ---------------- 屏幕 ----------------

async def list_screens(tenant_id: str) -> list[dict]:
    async with _get_session() as s:
        rows = (await s.execute(select(ParkScreen).where(ParkScreen.tenant_id == tenant_id).order_by(ParkScreen.created_at.desc()))).scalars().all()
        return [_sc(r) for r in rows]


async def create_screen(tenant_id: str, payload: dict) -> dict:
    row = ParkScreen(id=_new_id("SCR"), tenant_id=tenant_id, device_id=payload.get("device_id", ""),
                     name=payload.get("name", ""), role=payload.get("role", "main"),
                     location=payload.get("location", ""), created_at=_now())
    async with _get_session() as s:
        s.add(row)
        await s.commit()
    return _sc(row)


async def delete_screen(tenant_id: str, sid: str) -> bool:
    async with _get_session() as s:
        r = await s.execute(delete(ParkScreen).where(ParkScreen.id == sid, ParkScreen.tenant_id == tenant_id))
        await s.commit()
        return r.rowcount > 0


# ---------------- 设备绑定（大屏 → 园区） ----------------

async def get_device_by_id(device_id: str) -> dict | None:
    async with _get_session() as s:
        row = (await s.execute(select(ParkScreen).where(ParkScreen.device_id == device_id).limit(1))).scalars().first()
        return _sc(row) if row else None


async def ensure_device(device_id: str) -> dict:
    """确保存在该设备记录（未绑定）。"""
    existing = await get_device_by_id(device_id)
    if existing:
        return existing
    async with _get_session() as s:
        s.add(ParkScreen(id=_new_id("SCR"), device_id=device_id, status="unbound", tenant_id=None,
                         name="", role="main", location="", created_at=_now()))
        await s.commit()
    return await get_device_by_id(device_id)


async def set_device_code(device_id: str, code: str, expires: str) -> dict:
    async with _get_session() as s:
        row = (await s.execute(select(ParkScreen).where(ParkScreen.device_id == device_id).limit(1))).scalars().first()
        if row is None:
            return None
        row.code = code
        row.code_expires = expires
        row.status = "unbound"
        row.tenant_id = None
        await s.commit()
        await s.refresh(row)
    return _sc(row)


async def find_device_by_code(code: str) -> dict | None:
    async with _get_session() as s:
        row = (await s.execute(select(ParkScreen).where(ParkScreen.code == code, ParkScreen.status == "unbound").limit(1))).scalars().first()
        return _sc(row) if row else None


async def bind_device(tenant_id: str, device_id: str, name: str = "", role: str = "main", location: str = "") -> dict | None:
    async with _get_session() as s:
        row = (await s.execute(select(ParkScreen).where(ParkScreen.device_id == device_id).limit(1))).scalars().first()
        if row is None:
            return None
        row.tenant_id = tenant_id
        row.status = "bound"
        row.name = name or row.name or "未命名屏"
        row.role = role
        row.location = location
        row.bound_at = _now()
        row.code = ""
        row.code_expires = ""
        await s.commit()
        await s.refresh(row)
    return _sc(row)


async def unbind_device(device_id: str) -> bool:
    async with _get_session() as s:
        row = (await s.execute(select(ParkScreen).where(ParkScreen.device_id == device_id).limit(1))).scalars().first()
        if row is None:
            return False
        row.tenant_id = None
        row.status = "unbound"
        row.code = ""
        row.bound_at = ""
        await s.commit()
        return True


# ---------------- 大屏数据 ----------------

async def screen_view(tenant_id: str) -> dict:
    t = await get_tenant(tenant_id) or {}
    d = t.get("data", {})
    park = d.get("park", {}) or {}
    return {"tenant_id": tenant_id, "name": t.get("name", ""), "intro": t.get("intro", []),
        "founded": park.get("founded"), "province_level": park.get("province_level") or park.get("provinceLevel"), "area": park.get("area"),
        "region": park.get("region", ""), "address": park.get("address"), "phone": park.get("phone"), "email": park.get("email"),
        "capacity": d.get("projects", {}).get("capacity"), "invested": d.get("projects", {}).get("invested"),
        "jobs": d.get("jobs", {}).get("total"), "revenue_total": d.get("revenue", {}).get("total"), "revenue_tax": d.get("revenue", {}).get("tax"),
        "zones": d.get("zones", []), "industryMix": d.get("industryMix", []), "feed": d.get("feed", []), "as_of": d.get("as_of", "")}


async def update_screen(tenant_id: str, payload: dict) -> dict:
    async with _get_session() as s:
        row = await s.get(ParkTenant, tenant_id)
        d = dict(json.loads(row.data_json or "{}"))
        park = dict(d.get("park", {}))
        for k in ("founded", "area", "address", "phone", "email", "region"):
            if payload.get(k) is not None:
                park[k] = payload[k]
        d["park"] = park
        proj = dict(d.get("projects", {}))
        if payload.get("capacity") is not None: proj["capacity"] = payload["capacity"]
        if payload.get("invested") is not None: proj["invested"] = payload["invested"]
        d["projects"] = proj
        jobs = dict(d.get("jobs", {}))
        if payload.get("jobs") is not None: jobs["total"] = payload["jobs"]
        d["jobs"] = jobs
        rev = dict(d.get("revenue", {}))
        if payload.get("revenue_total") is not None: rev["total"] = payload["revenue_total"]
        if payload.get("revenue_tax") is not None: rev["tax"] = payload["revenue_tax"]
        d["revenue"] = rev
        for k in ("feed", "zones", "industryMix"):
            if payload.get(k) is not None: d[k] = payload[k]
        row.name = payload.get("name") or row.name
        row.intro_json = json.dumps(payload.get("intro") or json.loads(row.intro_json or "[]"), ensure_ascii=False)
        row.data_json = json.dumps(d, ensure_ascii=False)
        await s.commit()
        await s.refresh(row)
    refresh_engine(tenant_id, d)
    return await screen_view(tenant_id)
