from datetime import datetime
from enum import Enum
from typing import Any, Dict, List, Optional
from pydantic import BaseModel, ConfigDict, Field, model_validator


# ============ Enums ============
class AssistantConfigMode(str, Enum):
    PLATFORM = "platform"
    DIFY = "dify"
    FASTGPT = "fastgpt"
    NONE = "none"


class LLMModelType(str, Enum):
    TEXT = "text"
    EMBEDDING = "embedding"
    RERANK = "rerank"


class ASRLanguage(str, Enum):
    ZH = "zh"
    EN = "en"
    MULTILINGUAL = "Multi-lingual"


class VoiceGender(str, Enum):
    MALE = "Male"
    FEMALE = "Female"


class CallRecordSource(str, Enum):
    DEBUG = "debug"
    EXTERNAL = "external"


class CallRecordStatus(str, Enum):
    CONNECTED = "connected"
    MISSED = "missed"
    FAILED = "failed"


# ============ Voice ============
class VoiceBase(BaseModel):
    name: str
    vendor: str
    gender: str  # "Male" | "Female"
    language: str  # "zh" | "en"
    description: str = ""


class VoiceCreate(VoiceBase):
    id: Optional[str] = None
    model: Optional[str] = None  # 厂商语音模型标识
    voice_key: Optional[str] = None  # 厂商voice_key
    api_key: Optional[str] = None
    base_url: Optional[str] = None
    speed: float = 1.0
    gain: int = 0
    pitch: int = 0
    enabled: bool = True


class VoiceUpdate(BaseModel):
    name: Optional[str] = None
    vendor: Optional[str] = None
    gender: Optional[str] = None
    language: Optional[str] = None
    description: Optional[str] = None
    model: Optional[str] = None
    voice_key: Optional[str] = None
    api_key: Optional[str] = None
    base_url: Optional[str] = None
    speed: Optional[float] = None
    gain: Optional[int] = None
    pitch: Optional[int] = None
    enabled: Optional[bool] = None


class VoiceOut(VoiceBase):
    id: str
    user_id: Optional[int] = None
    model: Optional[str] = None
    voice_key: Optional[str] = None
    api_key: Optional[str] = None
    base_url: Optional[str] = None
    speed: float = 1.0
    gain: int = 0
    pitch: int = 0
    enabled: bool = True
    is_system: bool = False
    created_at: Optional[datetime] = None

    class Config:
        from_attributes = True


class VoicePreviewRequest(BaseModel):
    text: str
    api_key: Optional[str] = None
    speed: Optional[float] = None
    gain: Optional[int] = None
    pitch: Optional[int] = None


class VoicePreviewResponse(BaseModel):
    success: bool
    audio_url: Optional[str] = None
    duration_ms: Optional[int] = None
    error: Optional[str] = None


# ============ LLM Model ============
class LLMModelBase(BaseModel):
    name: str
    vendor: str
    type: LLMModelType
    base_url: str
    api_key: str
    model_name: Optional[str] = None
    temperature: Optional[float] = None
    context_length: Optional[int] = None
    enabled: bool = True


class LLMModelCreate(LLMModelBase):
    id: Optional[str] = None


class LLMModelUpdate(BaseModel):
    name: Optional[str] = None
    vendor: Optional[str] = None
    type: Optional[LLMModelType] = None
    base_url: Optional[str] = None
    api_key: Optional[str] = None
    model_name: Optional[str] = None
    temperature: Optional[float] = None
    context_length: Optional[int] = None
    enabled: Optional[bool] = None


class LLMModelOut(LLMModelBase):
    id: str
    user_id: int
    created_at: Optional[datetime] = None
    updated_at: Optional[datetime] = None

    class Config:
        from_attributes = True


class LLMModelTestResponse(BaseModel):
    success: bool
    latency_ms: Optional[int] = None
    message: Optional[str] = None


class LLMPreviewRequest(BaseModel):
    message: str
    system_prompt: Optional[str] = None
    max_tokens: Optional[int] = None
    temperature: Optional[float] = None
    api_key: Optional[str] = None


class LLMPreviewResponse(BaseModel):
    success: bool
    reply: Optional[str] = None
    usage: Optional[dict] = None
    latency_ms: Optional[int] = None
    error: Optional[str] = None


# ============ ASR Model ============
class ASRModelBase(BaseModel):
    name: str
    vendor: str
    language: str  # "zh" | "en" | "Multi-lingual"
    base_url: str
    api_key: str
    model_name: Optional[str] = None
    enabled: bool = True


class ASRModelCreate(ASRModelBase):
    id: Optional[str] = None
    hotwords: List[str] = []
    enable_punctuation: bool = True
    enable_normalization: bool = True


class ASRModelUpdate(BaseModel):
    name: Optional[str] = None
    language: Optional[str] = None
    base_url: Optional[str] = None
    api_key: Optional[str] = None
    model_name: Optional[str] = None
    hotwords: Optional[List[str]] = None
    enable_punctuation: Optional[bool] = None
    enable_normalization: Optional[bool] = None
    enabled: Optional[bool] = None


class ASRModelOut(ASRModelBase):
    id: str
    user_id: int
    hotwords: List[str] = []
    enable_punctuation: bool = True
    enable_normalization: bool = True
    created_at: Optional[datetime] = None

    class Config:
        from_attributes = True


class ASRTestRequest(BaseModel):
    audio_url: Optional[str] = None
    audio_data: Optional[str] = None  # base64 encoded


class ASRTestResponse(BaseModel):
    success: bool
    transcript: Optional[str] = None
    language: Optional[str] = None
    confidence: Optional[float] = None
    duration_ms: Optional[int] = None
    latency_ms: Optional[int] = None
    message: Optional[str] = None
    error: Optional[str] = None


# ============ Tool Resource ============
class ToolResourceBase(BaseModel):
    name: str
    description: str = ""
    category: str = "system"  # system/query
    icon: str = "Wrench"
    http_method: str = "GET"
    http_url: Optional[str] = None
    http_headers: Dict[str, str] = Field(default_factory=dict)
    http_timeout_ms: int = 10000
    enabled: bool = True


class ToolResourceCreate(ToolResourceBase):
    id: Optional[str] = None


class ToolResourceUpdate(BaseModel):
    name: Optional[str] = None
    description: Optional[str] = None
    category: Optional[str] = None
    icon: Optional[str] = None
    http_method: Optional[str] = None
    http_url: Optional[str] = None
    http_headers: Optional[Dict[str, str]] = None
    http_timeout_ms: Optional[int] = None
    enabled: Optional[bool] = None


class ToolResourceOut(ToolResourceBase):
    id: str
    user_id: Optional[int] = None
    is_system: bool = False
    created_at: Optional[datetime] = None
    updated_at: Optional[datetime] = None

    class Config:
        from_attributes = True


# ============ Assistant ============
class AssistantBase(BaseModel):
    name: str
    opener: str = ""
    prompt: str = ""
    knowledgeBaseId: Optional[str] = None
    language: str = "zh"
    voiceOutputEnabled: bool = True
    voice: Optional[str] = None
    speed: float = 1.0
    hotwords: List[str] = []
    tools: List[str] = []
    interruptionSensitivity: int = 500
    configMode: str = "platform"
    apiUrl: Optional[str] = None
    apiKey: Optional[str] = None
    # 模型关联
    llmModelId: Optional[str] = None
    asrModelId: Optional[str] = None
    embeddingModelId: Optional[str] = None
    rerankModelId: Optional[str] = None


class AssistantCreate(AssistantBase):
    pass


class AssistantUpdate(BaseModel):
    name: Optional[str] = None
    opener: Optional[str] = None
    prompt: Optional[str] = None
    knowledgeBaseId: Optional[str] = None
    language: Optional[str] = None
    voiceOutputEnabled: Optional[bool] = None
    voice: Optional[str] = None
    speed: Optional[float] = None
    hotwords: Optional[List[str]] = None
    tools: Optional[List[str]] = None
    interruptionSensitivity: Optional[int] = None
    configMode: Optional[str] = None
    apiUrl: Optional[str] = None
    apiKey: Optional[str] = None
    llmModelId: Optional[str] = None
    asrModelId: Optional[str] = None
    embeddingModelId: Optional[str] = None
    rerankModelId: Optional[str] = None


class AssistantOut(AssistantBase):
    id: str
    callCount: int = 0
    created_at: Optional[datetime] = None
    updated_at: Optional[datetime] = None

    class Config:
        from_attributes = True


class AssistantStats(BaseModel):
    assistant_id: str
    total_calls: int = 0
    connected_calls: int = 0
    missed_calls: int = 0
    avg_duration_seconds: float = 0.0
    today_calls: int = 0


# ============ Knowledge Base ============
class KnowledgeDocument(BaseModel):
    id: str
    name: str
    size: str
    fileType: str = "txt"
    storageUrl: Optional[str] = None
    status: str = "pending"
    chunkCount: int = 0
    uploadDate: str


class KnowledgeDocumentCreate(BaseModel):
    name: str
    size: str
    fileType: str = "txt"
    storageUrl: Optional[str] = None


class KnowledgeDocumentUpdate(BaseModel):
    status: Optional[str] = None
    chunkCount: Optional[int] = None
    errorMessage: Optional[str] = None


class KnowledgeBaseBase(BaseModel):
    name: str
    description: str = ""
    embeddingModel: str = "text-embedding-3-small"
    chunkSize: int = 500
    chunkOverlap: int = 50


class KnowledgeBaseCreate(KnowledgeBaseBase):
    pass


class KnowledgeBaseUpdate(BaseModel):
    name: Optional[str] = None
    description: Optional[str] = None
    embeddingModel: Optional[str] = None
    chunkSize: Optional[int] = None
    chunkOverlap: Optional[int] = None
    status: Optional[str] = None


class KnowledgeBaseOut(KnowledgeBaseBase):
    id: str
    docCount: int = 0
    chunkCount: int = 0
    status: str = "active"
    createdAt: Optional[datetime] = None
    updatedAt: Optional[datetime] = None
    documents: List[KnowledgeDocument] = []

    class Config:
        from_attributes = True


# ============ Knowledge Search ============
class KnowledgeSearchQuery(BaseModel):
    query: str
    kb_id: str
    nResults: int = 5


class KnowledgeSearchResult(BaseModel):
    query: str
    results: List[dict]


class DocumentIndexRequest(BaseModel):
    document_id: str
    content: str


class KnowledgeStats(BaseModel):
    kb_id: str
    docCount: int
    chunkCount: int


# ============ Workflow ============
class WorkflowNode(BaseModel):
    model_config = ConfigDict(extra="allow")

    id: Optional[str] = None
    name: str = ""
    type: str = "assistant"
    isStart: Optional[bool] = None
    metadata: Dict[str, Any] = Field(default_factory=dict)
    prompt: Optional[str] = None
    messagePlan: Optional[Dict[str, Any]] = None
    variableExtractionPlan: Optional[Dict[str, Any]] = None
    tool: Optional[Dict[str, Any]] = None
    globalNodePlan: Optional[Dict[str, Any]] = None
    assistantId: Optional[str] = None
    assistant: Optional[Dict[str, Any]] = None

    @model_validator(mode="before")
    @classmethod
    def _normalize_legacy_node(cls, data: Any) -> Any:
        if not isinstance(data, dict):
            return data
        raw = dict(data)
        node_id = raw.get("id") or raw.get("name")
        if not node_id:
            node_id = f"node_{abs(hash(str(raw))) % 100000}"
        raw["id"] = str(node_id)
        raw["name"] = str(raw.get("name") or raw["id"])

        node_type = str(raw.get("type") or "assistant").lower()
        if node_type == "conversation":
            node_type = "assistant"
        elif node_type == "human":
            node_type = "human_transfer"
        elif node_type not in {"start", "assistant", "tool", "human_transfer", "end"}:
            node_type = "assistant"
        raw["type"] = node_type

        metadata = raw.get("metadata")
        if not isinstance(metadata, dict):
            metadata = {}
        if "position" not in metadata and isinstance(raw.get("position"), dict):
            metadata["position"] = raw.get("position")
        raw["metadata"] = metadata

        if raw.get("isStart") is None and node_type == "start":
            raw["isStart"] = True
        return raw


class WorkflowEdge(BaseModel):
    model_config = ConfigDict(extra="allow")

    id: Optional[str] = None
    fromNodeId: str
    toNodeId: str
    label: Optional[str] = None
    condition: Optional[Dict[str, Any]] = None
    priority: int = 100

    @model_validator(mode="before")
    @classmethod
    def _normalize_legacy_edge(cls, data: Any) -> Any:
        if not isinstance(data, dict):
            return data
        raw = dict(data)
        from_node = raw.get("fromNodeId") or raw.get("from") or raw.get("from_") or raw.get("source")
        to_node = raw.get("toNodeId") or raw.get("to") or raw.get("target")
        raw["fromNodeId"] = str(from_node or "")
        raw["toNodeId"] = str(to_node or "")
        if raw.get("id") is None:
            raw["id"] = f"e_{raw['fromNodeId']}_{raw['toNodeId']}"
        if raw.get("condition") is None:
            if raw.get("label"):
                raw["condition"] = {"type": "contains", "source": "user", "value": str(raw["label"])}
            else:
                raw["condition"] = {"type": "always"}
        return raw


class WorkflowBase(BaseModel):
    name: str
    nodeCount: int = 0
    createdAt: str = ""
    updatedAt: str = ""
    globalPrompt: Optional[str] = None
    nodes: List[WorkflowNode] = Field(default_factory=list)
    edges: List[WorkflowEdge] = Field(default_factory=list)


class WorkflowCreate(WorkflowBase):
    @model_validator(mode="after")
    def _validate_graph(self) -> "WorkflowCreate":
        _validate_workflow_graph(self.nodes, self.edges)
        return self


class WorkflowUpdate(BaseModel):
    name: Optional[str] = None
    nodeCount: Optional[int] = None
    nodes: Optional[List[WorkflowNode]] = None
    edges: Optional[List[WorkflowEdge]] = None
    globalPrompt: Optional[str] = None

    @model_validator(mode="after")
    def _validate_partial_graph(self) -> "WorkflowUpdate":
        if self.nodes is not None and self.edges is not None:
            _validate_workflow_graph(self.nodes, self.edges)
        return self


class WorkflowOut(WorkflowBase):
    id: str

    @model_validator(mode="before")
    @classmethod
    def _normalize_db_fields(cls, data: Any) -> Any:
        if isinstance(data, dict):
            raw = dict(data)
        else:
            raw = {
                "id": getattr(data, "id", None),
                "name": getattr(data, "name", None),
                "node_count": getattr(data, "node_count", None),
                "created_at": getattr(data, "created_at", None),
                "updated_at": getattr(data, "updated_at", None),
                "global_prompt": getattr(data, "global_prompt", None),
                "nodes": getattr(data, "nodes", None),
                "edges": getattr(data, "edges", None),
            }

        if "nodeCount" not in raw and raw.get("node_count") is not None:
            raw["nodeCount"] = raw["node_count"]
        if "createdAt" not in raw and raw.get("created_at") is not None:
            raw["createdAt"] = raw["created_at"]
        if "updatedAt" not in raw and raw.get("updated_at") is not None:
            raw["updatedAt"] = raw["updated_at"]
        if "globalPrompt" not in raw and raw.get("global_prompt") is not None:
            raw["globalPrompt"] = raw["global_prompt"]
        return raw

    class Config:
        from_attributes = True


def _validate_workflow_graph(nodes: List[WorkflowNode], edges: List[WorkflowEdge]) -> None:
    if not nodes:
        raise ValueError("Workflow must include at least one node")

    node_ids = [node.id for node in nodes if node.id]
    if len(node_ids) != len(set(node_ids)):
        raise ValueError("Workflow node ids must be unique")

    starts = [node for node in nodes if node.isStart or node.type == "start"]
    if not starts:
        raise ValueError("Workflow must define a start node (isStart=true or type=start)")

    known = set(node_ids)
    for edge in edges:
        if edge.fromNodeId not in known:
            raise ValueError(f"Workflow edge fromNodeId not found: {edge.fromNodeId}")
        if edge.toNodeId not in known:
            raise ValueError(f"Workflow edge toNodeId not found: {edge.toNodeId}")


# ============ Call Record ============
class TranscriptSegment(BaseModel):
    turnIndex: int
    speaker: str  # human/ai
    content: str
    confidence: Optional[float] = None
    startMs: int
    endMs: int
    durationMs: Optional[int] = None
    audioUrl: Optional[str] = None
    emotion: Optional[str] = None


class CallRecordCreate(BaseModel):
    user_id: int
    assistant_id: Optional[str] = None
    source: str = "debug"
    status: Optional[str] = None
    cost: Optional[float] = None


class CallRecordUpdate(BaseModel):
    status: Optional[str] = None
    summary: Optional[str] = None
    duration_seconds: Optional[int] = None
    ended_at: Optional[str] = None
    cost: Optional[float] = None
    metadata: Optional[dict] = None


class CallRecordOut(BaseModel):
    id: str
    user_id: int
    assistant_id: Optional[str] = None
    source: str
    status: str
    started_at: str
    ended_at: Optional[str] = None
    duration_seconds: Optional[int] = None
    summary: Optional[str] = None
    cost: float = 0.0
    metadata: dict = {}
    created_at: Optional[datetime] = None
    transcripts: List[TranscriptSegment] = []

    class Config:
        from_attributes = True


# ============ Call Transcript ============
class TranscriptCreate(BaseModel):
    turn_index: int
    speaker: str
    content: str
    confidence: Optional[float] = None
    start_ms: int
    end_ms: int
    duration_ms: Optional[int] = None
    emotion: Optional[str] = None


class TranscriptOut(TranscriptCreate):
    id: int
    audio_url: Optional[str] = None

    class Config:
        from_attributes = True


# ============ History Stats ============
class HistoryStats(BaseModel):
    total_calls: int = 0
    connected_calls: int = 0
    missed_calls: int = 0
    failed_calls: int = 0
    avg_duration_seconds: float = 0.0
    total_cost: float = 0.0
    by_status: dict = {}
    by_source: dict = {}
    daily_trend: List[dict] = []


# ============ Dashboard ============
class DashboardStats(BaseModel):
    totalCalls: int
    answerRate: int
    avgDuration: str
    humanTransferCount: int
    trend: List[dict]


# ============ API Response ============
class Message(BaseModel):
    message: str


class DocumentIndexRequest(BaseModel):
    content: str


class ListResponse(BaseModel):
    total: int
    page: int
    limit: int
    list: List


class SearchResult(BaseModel):
    id: str
    started_at: str
    matched_content: Optional[str] = None
