"""U8 — TaskIQ 异步任务集成 — 文档向量化与批量索引。

包装 TaskIQ broker，提供：
- 任务参数 schema 校验（Pydantic 模型）
- per-user 并发上限
- 任务状态机（PENDING → RUNNING → COMPLETED | FAILED | CANCELLED）
- Worker liveness & sweeper（worker_heartbeat_at 超时检测）
- 错误消息净化（剥离内部路径/连接串）
- 降级模式：broker 未配置时同步执行

Redis 隔离：使用独立 db=1（与 bus/cache 的 db=0 分离），或 key 前缀 ``taskiq:``。
任务参数在 Redis 中带 TTL（默认 1 小时）。

ponytail: TaskIQ broker 为可选依赖 — 通过 TYPE_CHECKING 导入，
未安装时模块仍可加载，TaskManager 退化为同步执行。
"""

from __future__ import annotations

import asyncio
import inspect
import logging
import re
import uuid
from datetime import datetime, timedelta, timezone
from typing import TYPE_CHECKING, Protocol

from pydantic import BaseModel, ConfigDict, Field, field_validator

from agentkit.core.protocol import TaskStatus
from agentkit.rag_platform.document_processor import (
    DEFAULT_CHUNK_OVERLAP,
    DEFAULT_CHUNK_SIZE,
    DocumentProcessor,
)
from agentkit.server.task_store import InMemoryTaskStore, TaskRecord

if TYPE_CHECKING:
    from llama_index.core.embeddings import BaseEmbedding
    from llama_index.vector_stores.postgres import PGVectorStore

    from agentkit.rag_platform.store import KBStore

logger = logging.getLogger(__name__)

# 默认配置
DEFAULT_MAX_CONCURRENT_PER_USER = 3
DEFAULT_WORKER_TTL_SECONDS = 300  # worker 心跳超时阈值
DEFAULT_ERROR_MESSAGE_MAX_LENGTH = 500
DEFAULT_TASK_TTL_SECONDS = 3600  # 任务参数 TTL

# Redis 隔离 — 独立 db=1，与 bus/cache 的 db=0 分离
TASKIQ_REDIS_DB = 1
TASKIQ_KEY_PREFIX = "taskiq:"


async def _maybe_await(result: object) -> object:
    """统一处理 sync/async 调用结果 — InMemoryTaskStore 方法为 sync，RedisTaskStore 为 async。"""
    if inspect.isawaitable(result):
        return await result
    return result


# ---------------------------------------------------------------------------
# 任务参数模型 — schema 校验
# ---------------------------------------------------------------------------


class VectorizeTaskParams(BaseModel):
    """向量化任务参数 — 单文档 parse → segment → vectorize → index。"""

    model_config = ConfigDict()

    document_id: str = Field(min_length=1, max_length=128)
    kb_id: str = Field(min_length=1, max_length=128)
    file_path: str = Field(min_length=1, max_length=1024)
    file_type: str = Field(min_length=1, max_length=64)
    chunk_size: int = Field(default=DEFAULT_CHUNK_SIZE, ge=50, le=8192)
    chunk_overlap: int = Field(default=DEFAULT_CHUNK_OVERLAP, ge=0, le=1024)
    user_id: str = Field(min_length=1, max_length=128)

    @field_validator("chunk_overlap")
    @classmethod
    def _overlap_less_than_size(cls, v: int, info: object) -> int:
        """chunk_overlap 必须小于 chunk_size。"""
        size = info.data.get("chunk_size", DEFAULT_CHUNK_SIZE)
        if v >= size:
            raise ValueError(f"chunk_overlap ({v}) must be < chunk_size ({size})")
        return v


class BatchIndexTaskParams(BaseModel):
    """批量索引任务参数 — 多文档并行向量化。"""

    model_config = ConfigDict()

    kb_id: str = Field(min_length=1, max_length=128)
    document_ids: list[str] = Field(min_length=1, max_length=100)
    user_id: str = Field(min_length=1, max_length=128)
    file_paths: dict[str, str] = Field(default_factory=dict)  # document_id -> file_path
    file_types: dict[str, str] = Field(default_factory=dict)  # document_id -> file_type
    chunk_size: int = Field(default=DEFAULT_CHUNK_SIZE, ge=50, le=8192)
    chunk_overlap: int = Field(default=DEFAULT_CHUNK_OVERLAP, ge=0, le=1024)


# ---------------------------------------------------------------------------
# 错误消息净化
# ---------------------------------------------------------------------------

# 匹配 Unix/Windows 文件路径
_PATH_RE = re.compile(
    r"(?:/[\w.\-]+)+|"  # Unix 路径 /usr/local/...
    r"(?:[A-Za-z]:\\[\w.\-]+(?:\\[\w.\-]+)*)"  # Windows 路径 C:\Users\...
)
# 匹配 Redis/PostgreSQL 连接串
_CONN_STR_RE = re.compile(
    r"(redis://|postgresql://|postgres://|mysql://)"
    r"[^\s\"']+",
    re.IGNORECASE,
)
# 匹配环境变量风格的密钥
_SECRET_RE = re.compile(
    r"(api[_-]?key|token|password|secret)[\s:=]+[\w\-]+",
    re.IGNORECASE,
)


def sanitize_error_message(msg: str, max_length: int = DEFAULT_ERROR_MESSAGE_MAX_LENGTH) -> str:
    """净化错误消息 — 剥离内部路径、连接串、密钥。

    Args:
        msg: 原始错误消息
        max_length: 最大长度（截断）

    Returns:
        净化后的错误消息
    """
    if not msg:
        return ""
    # 连接串优先于路径 — 避免 /localhost:6379/0 被路径正则部分匹配
    cleaned = _CONN_STR_RE.sub("<connection-string>", msg)
    cleaned = _PATH_RE.sub("<path>", cleaned)
    cleaned = _SECRET_RE.sub(r"\1=<redacted>", cleaned)
    if len(cleaned) > max_length:
        cleaned = cleaned[: max_length - 3] + "..."
    return cleaned


# ---------------------------------------------------------------------------
# Worker liveness & sweeper
# ---------------------------------------------------------------------------


class TaskStoreProtocol(Protocol):
    """TaskStore 协议 — InMemoryTaskStore / RedisTaskStore 均满足。"""

    async def create(
        self, task_id: str, agent_name: str, input_data: dict, skill_name: str | None = None
    ) -> TaskRecord: ...

    def get(self, task_id: str) -> TaskRecord | None: ...

    async def update_status(
        self, task_id: str, status: TaskStatus, **kwargs: object
    ) -> TaskRecord: ...

    def list_tasks(
        self, status: TaskStatus | None = None, limit: int = 100
    ) -> list[TaskRecord]: ...


class WorkerSweeper:
    """检测超时任务并标记为 failed。

    扫描 status=RUNNING 且 worker_heartbeat_at 超过 TTL 的任务，
    将其状态置为 failed，error_message="worker_timeout"。

    ponytail: 升级路径 — 当前依赖 list_tasks 全量扫描（O(n)），
    生产环境可改为 Redis ZSET 按心跳时间戳排序（O(log n)）。
    """

    def __init__(
        self,
        task_store: TaskStoreProtocol,
        ttl_seconds: int = DEFAULT_WORKER_TTL_SECONDS,
    ) -> None:
        self._store = task_store
        self._ttl = ttl_seconds

    async def sweep(self) -> int:
        """扫描超时任务，返回清理数量。"""
        now = datetime.now(timezone.utc)
        cutoff = now - timedelta(seconds=self._ttl)
        cleaned = 0

        running_tasks = await _maybe_await(
            self._store.list_tasks(status=TaskStatus.RUNNING, limit=1000)
        )

        for record in running_tasks:
            heartbeat = record.metadata.get("worker_heartbeat_at")
            if heartbeat is None:
                # 无心跳记录 — 使用 started_at 作为回退
                heartbeat = record.started_at.isoformat() if record.started_at else None
            if heartbeat is None:
                continue

            try:
                hb_time = datetime.fromisoformat(heartbeat)
            except (ValueError, TypeError):
                continue

            # 确保 timezone-aware
            if hb_time.tzinfo is None:
                hb_time = hb_time.replace(tzinfo=timezone.utc)

            if hb_time < cutoff:
                try:
                    await _maybe_await(
                        self._store.update_status(
                            record.task_id,
                            TaskStatus.FAILED,
                            error_message="worker_timeout",
                            completed_at=now,
                            metadata={**record.metadata, "swept_at": now.isoformat()},
                        )
                    )
                    cleaned += 1
                    logger.warning(
                        "Swept timed-out task %s (heartbeat=%s, cutoff=%s)",
                        record.task_id,
                        heartbeat,
                        cutoff.isoformat(),
                    )
                except KeyError:
                    # 任务已被并发删除
                    continue
        return cleaned


# ---------------------------------------------------------------------------
# TaskManager — 任务提交、查询、取消
# ---------------------------------------------------------------------------


class ConcurrencyLimitExceeded(Exception):
    """per-user 并发上限超出。"""


class TaskNotFoundError(Exception):
    """任务不存在。"""


class TaskManager:
    """异步任务管理器 — 包装 TaskIQ broker。

    如果 broker 未配置或 TaskIQ 不可用，任务同步执行（降级模式）。

    Args:
        broker: TaskIQ broker 实例（可选）
        task_store: 任务状态存储（默认 InMemoryTaskStore）
        max_concurrent_per_user: per-user 并发上限
        task_ttl_seconds: 任务参数 TTL（Redis 模式）
    """

    def __init__(
        self,
        broker: object | None = None,
        task_store: TaskStoreProtocol | None = None,
        max_concurrent_per_user: int = DEFAULT_MAX_CONCURRENT_PER_USER,
        task_ttl_seconds: int = DEFAULT_TASK_TTL_SECONDS,
    ) -> None:
        self._broker = broker
        self._store: TaskStoreProtocol = task_store or InMemoryTaskStore()
        self._max_concurrent = max_concurrent_per_user
        self._task_ttl = task_ttl_seconds
        # user_id -> 当前 RUNNING 任务数
        self._running_counts: dict[str, int] = {}
        self._lock = asyncio.Lock()

    @property
    def store(self) -> TaskStoreProtocol:
        """暴露底层 task store（供 sweeper 使用）。"""
        return self._store

    @property
    def broker(self) -> object:
        """暴露底层 broker（供 startup/shutdown 集成）。"""
        return self._broker

    # ── 任务提交 ────────────────────────────────────────────────

    async def submit_vectorize(
        self,
        params: VectorizeTaskParams,
        dependencies: dict[str, object] | None = None,
    ) -> str:
        """提交向量化任务，返回 task_id。

        Args:
            params: 向量化任务参数（已通过 schema 校验）
            dependencies: 运行时依赖（store/vector_store/embed_model），
                降级模式下直接使用；broker 模式下由 worker 注入

        Raises:
            ConcurrencyLimitExceeded: per-user 并发上限超出
        """
        await self._check_concurrency(params.user_id)

        task_id = f"vec-{uuid.uuid4()}"
        input_data = params.model_dump()
        # 依赖对象不序列化 — 仅在降级模式内存中传递
        deps = dependencies or {}

        await _maybe_await(
            self._store.create(
                task_id=task_id,
                agent_name="rag_vectorize",
                input_data=input_data,
                skill_name="vectorize",
            )
        )
        # 记录 user_id 到 metadata 便于按用户查询
        record = await _maybe_await(self._store.get(task_id))
        if record is not None:
            record.metadata["user_id"] = params.user_id

        logger.info(
            "Submitted vectorize task %s for document=%s user=%s",
            task_id,
            params.document_id,
            params.user_id,
        )

        if self._broker is not None:
            # broker 模式 — 异步派发
            await self._dispatch_to_broker("vectorize", input_data, task_id)
        else:
            # 降级模式 — 同步执行（不阻塞调用方，使用 asyncio.create_task）
            asyncio.create_task(self._run_vectorize_degraded(task_id, params, deps))

        return task_id

    async def submit_batch_index(
        self,
        params: BatchIndexTaskParams,
        dependencies: dict[str, object] | None = None,
    ) -> str:
        """提交批量索引任务，返回 task_id。"""
        await self._check_concurrency(params.user_id)

        task_id = f"batch-{uuid.uuid4()}"
        input_data = params.model_dump()
        deps = dependencies or {}

        await _maybe_await(
            self._store.create(
                task_id=task_id,
                agent_name="rag_batch_index",
                input_data=input_data,
                skill_name="batch_index",
            )
        )
        record = await _maybe_await(self._store.get(task_id))
        if record is not None:
            record.metadata["user_id"] = params.user_id

        logger.info(
            "Submitted batch index task %s for %d documents user=%s",
            task_id,
            len(params.document_ids),
            params.user_id,
        )

        if self._broker is not None:
            await self._dispatch_to_broker("batch_index", input_data, task_id)
        else:
            asyncio.create_task(self._run_batch_index_degraded(task_id, params, deps))

        return task_id

    # ── 任务查询 ────────────────────────────────────────────────

    async def get_task_status(self, task_id: str) -> TaskRecord | None:
        """查询任务状态。"""
        return await _maybe_await(self._store.get(task_id))

    async def list_tasks(
        self,
        user_id: str | None = None,
        status: TaskStatus | None = None,
        limit: int = 100,
    ) -> list[TaskRecord]:
        """查询任务历史 — 可按 user_id 和 status 过滤。"""
        tasks = await _maybe_await(self._store.list_tasks(status=status, limit=limit))
        if user_id is not None:
            tasks = [t for t in tasks if t.metadata.get("user_id") == user_id]
        return tasks

    async def cancel_task(self, task_id: str) -> bool:
        """取消任务 — 仅 PENDING/RUNNING 可取消。

        Returns:
            True 如果取消成功
        Raises:
            TaskNotFoundError: 任务不存在
        """
        record = await _maybe_await(self._store.get(task_id))
        if record is None:
            raise TaskNotFoundError(f"Task {task_id} not found")

        if record.status in (TaskStatus.COMPLETED, TaskStatus.FAILED, TaskStatus.CANCELLED):
            return False

        await _maybe_await(
            self._store.update_status(
                task_id,
                TaskStatus.CANCELLED,
                completed_at=datetime.now(timezone.utc),
            )
        )
        # 释放并发计数
        user_id = record.metadata.get("user_id")
        if user_id and self._running_counts.get(user_id, 0) > 0:
            self._running_counts[user_id] -= 1
        return True

    # ── 心跳更新 ────────────────────────────────────────────────

    async def update_heartbeat(self, task_id: str) -> None:
        """更新 worker 心跳时间戳 — 由 worker 周期性调用。"""
        record = await _maybe_await(self._store.get(task_id))
        if record is None:
            return
        now = datetime.now(timezone.utc).isoformat()
        record.metadata["worker_heartbeat_at"] = now

    # ── 内部方法 ────────────────────────────────────────────────

    async def _check_concurrency(self, user_id: str) -> None:
        """检查 per-user 并发上限。"""
        async with self._lock:
            current = self._running_counts.get(user_id, 0)
            if current >= self._max_concurrent:
                raise ConcurrencyLimitExceeded(
                    f"User {user_id} has {current} running tasks (max={self._max_concurrent})"
                )
            self._running_counts[user_id] = current + 1

    async def _release_concurrency(self, user_id: str) -> None:
        """释放并发计数。"""
        async with self._lock:
            if self._running_counts.get(user_id, 0) > 0:
                self._running_counts[user_id] -= 1

    async def _dispatch_to_broker(self, task_type: str, params: dict, task_id: str) -> None:
        """派发任务到 TaskIQ broker。"""
        # broker.kiq 返回 TaskiqTask — 实际执行由 worker 进程完成
        try:
            await self._broker.kiq(task_type=task_type, params=params, task_id=task_id)
        except Exception as e:
            logger.error("Failed to dispatch task %s to broker: %s", task_id, e)
            raise

    async def _run_vectorize_degraded(
        self,
        task_id: str,
        params: VectorizeTaskParams,
        deps: dict[str, object],
    ) -> None:
        """降级模式 — 同步执行向量化任务（在 asyncio 任务中）。"""
        now = datetime.now(timezone.utc)
        try:
            await _maybe_await(
                self._store.update_status(task_id, TaskStatus.RUNNING, started_at=now)
            )
            await self.update_heartbeat(task_id)

            await run_vectorize_task(params, **deps)

            await _maybe_await(
                self._store.update_status(
                    task_id,
                    TaskStatus.COMPLETED,
                    completed_at=datetime.now(timezone.utc),
                    progress=1.0,
                )
            )
        except Exception as e:
            sanitized = sanitize_error_message(str(e))
            await _maybe_await(
                self._store.update_status(
                    task_id,
                    TaskStatus.FAILED,
                    error_message=sanitized,
                    completed_at=datetime.now(timezone.utc),
                )
            )
            logger.error("Vectorize task %s failed: %s", task_id, sanitized)
        finally:
            await self._release_concurrency(params.user_id)

    async def _run_batch_index_degraded(
        self,
        task_id: str,
        params: BatchIndexTaskParams,
        deps: dict[str, object],
    ) -> None:
        """降级模式 — 同步执行批量索引任务。"""
        now = datetime.now(timezone.utc)
        total = len(params.document_ids)
        succeeded: list[str] = []
        failed: list[str] = []

        try:
            await _maybe_await(
                self._store.update_status(task_id, TaskStatus.RUNNING, started_at=now)
            )
            await self.update_heartbeat(task_id)

            for i, doc_id in enumerate(params.document_ids):
                await self.update_heartbeat(task_id)
                try:
                    single_params = VectorizeTaskParams(
                        document_id=doc_id,
                        kb_id=params.kb_id,
                        file_path=params.file_paths.get(doc_id, ""),
                        file_type=params.file_types.get(doc_id, "txt"),
                        chunk_size=params.chunk_size,
                        chunk_overlap=params.chunk_overlap,
                        user_id=params.user_id,
                    )
                    await run_vectorize_task(single_params, **deps)
                    succeeded.append(doc_id)
                except Exception as e:
                    logger.warning("Batch item %s failed: %s", doc_id, e)
                    failed.append(doc_id)

                # 更新进度
                progress = (i + 1) / total
                await _maybe_await(
                    self._store.update_status(
                        task_id,
                        TaskStatus.RUNNING,
                        progress=progress,
                        progress_message=f"Processed {i + 1}/{total}",
                    )
                )

            final_status = TaskStatus.COMPLETED if not failed else TaskStatus.PARTIALLY_COMPLETED
            await _maybe_await(
                self._store.update_status(
                    task_id,
                    final_status,
                    completed_at=datetime.now(timezone.utc),
                    progress=1.0,
                    output_data={
                        "succeeded": succeeded,
                        "failed": failed,
                        "total": total,
                    },
                )
            )
        except Exception as e:
            sanitized = sanitize_error_message(str(e))
            await _maybe_await(
                self._store.update_status(
                    task_id,
                    TaskStatus.FAILED,
                    error_message=sanitized,
                    completed_at=datetime.now(timezone.utc),
                )
            )
            logger.error("Batch index task %s failed: %s", task_id, sanitized)
        finally:
            await self._release_concurrency(params.user_id)


# ---------------------------------------------------------------------------
# 任务执行函数 — 由 worker 调用
# ---------------------------------------------------------------------------


async def run_vectorize_task(
    params: VectorizeTaskParams,
    store: "KBStore",
    vector_store: "PGVectorStore",
    embed_model: "BaseEmbedding",
) -> None:
    """向量化任务 — 由 worker 执行。

    封装 DocumentProcessor.process() — parse → segment → vectorize → index。
    状态转换由 DocumentProcessor 内部管理（pending → parsing → ... → indexed | failed）。

    Args:
        params: 向量化任务参数
        store: KBStore 实例（用于文档状态更新）
        vector_store: LlamaIndex PGVectorStore 实例
        embed_model: LlamaIndex embedding 模型

    Raises:
        Exception: 管道任一阶段失败
    """
    processor = DocumentProcessor(
        chunk_size=params.chunk_size,
        chunk_overlap=params.chunk_overlap,
    )
    await processor.process(
        params.file_path,
        params.file_type,
        params.kb_id,
        params.document_id,
        vector_store,
        embed_model,
        store,
    )


async def run_batch_index_task(
    params: BatchIndexTaskParams,
    store: "KBStore",
    vector_store: "PGVectorStore",
    embed_model: "BaseEmbedding",
) -> dict[str, list[str]]:
    """批量索引任务 — 顺序处理多个文档。

    Returns:
        {"succeeded": [...], "failed": [...]}
    """
    succeeded: list[str] = []
    failed: list[str] = []

    for doc_id in params.document_ids:
        try:
            single_params = VectorizeTaskParams(
                document_id=doc_id,
                kb_id=params.kb_id,
                file_path=params.file_paths.get(doc_id, ""),
                file_type=params.file_types.get(doc_id, "txt"),
                chunk_size=params.chunk_size,
                chunk_overlap=params.chunk_overlap,
                user_id=params.user_id,
            )
            await run_vectorize_task(single_params, store, vector_store, embed_model)
            succeeded.append(doc_id)
        except Exception as e:
            logger.warning("Batch item %s failed: %s", doc_id, e)
            failed.append(doc_id)

    return {"succeeded": succeeded, "failed": failed}


# ---------------------------------------------------------------------------
# TaskIQ broker 工厂（可选 — 仅 Redis 可用时使用）
# ---------------------------------------------------------------------------


def create_broker(redis_url: str) -> object:
    """创建 TaskIQ Redis broker — 独立 db=1 隔离。

    Args:
        redis_url: Redis 连接 URL（db=0 用于 bus/cache，此处强制改为 db=1）

    Returns:
        配置好的 TaskiqBroker 实例

    Raises:
        ImportError: taskiq 未安装
    """
    from urllib.parse import urlparse, urlunparse

    from taskiq_redis import ListQueueBroker, RedisAsyncResultBackend

    from taskiq import TaskiqBroker

    # 强制使用 db=1 — 与 bus/cache 的 db=0 隔离
    parsed = urlparse(redis_url)
    # 替换 path 为 /1
    new_path = "/1"
    isolated_url = urlunparse(parsed._replace(path=new_path))

    broker = TaskiqBroker(ListQueueBroker(url=isolated_url))
    broker.with_result_backend(
        RedisAsyncResultBackend(redis_url=isolated_url, result_ex_time=DEFAULT_TASK_TTL_SECONDS)
    )

    logger.info("TaskIQ broker created with Redis db=1 isolation")
    return broker


__all__ = [
    "BatchIndexTaskParams",
    "ConcurrencyLimitExceeded",
    "DEFAULT_MAX_CONCURRENT_PER_USER",
    "DEFAULT_WORKER_TTL_SECONDS",
    "TaskManager",
    "TaskNotFoundError",
    "TaskStoreProtocol",
    "VectorizeTaskParams",
    "WorkerSweeper",
    "create_broker",
    "run_batch_index_task",
    "run_vectorize_task",
    "sanitize_error_message",
]
