use anyhow::Result;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::json;
use sqlx::{postgres::PgPoolOptions, PgPool, Row};
use std::sync::Arc;
use tokio::sync::RwLock;
use uuid::Uuid;

use super::{schema, Database, QdrantDb};
use crate::core::chunk::types::{Chunk, ChunkRule, ChunkType};
use crate::core::text::synonym::normalize_chinese_query;
use crate::core::text::{
    global_synonym_expander,
    tokenizer::{contains_chinese, tokenize_chinese_text},
};

#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct ResourceRecord {
    pub resource_id: String,
    pub resource_type: String,
    pub category: String,
    pub capabilities: Option<serde_json::Value>,
    pub config: Option<serde_json::Value>,
    pub metadata: Option<serde_json::Value>,
    pub status: String,
    pub last_heartbeat: Option<chrono::DateTime<chrono::Utc>>,
    pub created_at: Option<chrono::DateTime<chrono::Utc>>,
}

#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct IdentityRecord {
    pub id: i32,
    pub uuid: Uuid,
    pub name: String,
    pub metadata: serde_json::Value,
    pub created_at: Option<chrono::DateTime<chrono::Utc>>,
}

#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct FileRecord {
    pub uuid: String,
    pub file_path: String,
    pub file_name: String,
    pub probe_json: Option<serde_json::Value>,
    pub created_at: Option<chrono::DateTime<chrono::Utc>>,
}

#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct CandidateRecord {
    pub id: i64,
    pub file_uuid: Uuid,
    pub data: serde_json::Value, // Face data
    pub created_at: Option<chrono::DateTime<chrono::Utc>>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct StorageStatus {
    pub fs_video: bool,
    pub fs_json: bool,
    pub psql_chunk: bool,
    pub pobject_chunk: bool,
    pub mobject_chunk: bool,
    pub pvector_chunk: bool,
    pub qvector_chunk: bool,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum VideoStatus {
    Pending,
    Processing,
    Completed,
    Failed,
}

impl VideoStatus {
    pub fn as_str(&self) -> &'static str {
        match self {
            VideoStatus::Pending => "pending",
            VideoStatus::Processing => "processing",
            VideoStatus::Completed => "completed",
            VideoStatus::Failed => "failed",
        }
    }

    pub fn from_db_str(s: &str) -> Option<Self> {
        match s {
            "pending" => Some(VideoStatus::Pending),
            "processing" => Some(VideoStatus::Processing),
            "completed" => Some(VideoStatus::Completed),
            "failed" => Some(VideoStatus::Failed),
            _ => None,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct VideoRow {
    pub id: i32,
    pub uuid: String,
    pub file_path: String,
    pub file_name: String,
    pub duration: f64,
    pub width: i32,
    pub height: i32,
    pub fps: f64,
    pub probe_json: Option<String>,
    pub fs_video: bool,
    pub fs_json: bool,
    pub psql_chunk: bool,
    pub pobject_chunk: bool,
    pub mobject_chunk: bool,
    pub pvector_chunk: bool,
    pub qvector_chunk: bool,
    pub status: String,
    pub user_id: Option<i32>,
    pub job_id: Option<i32>,
    pub created_at: Option<String>,
    pub registration_time: Option<String>,
}

impl From<VideoRow> for VideoRecord {
    fn from(row: VideoRow) -> Self {
        VideoRecord {
            id: row.id as i64,
            uuid: row.uuid,
            file_path: row.file_path,
            file_name: row.file_name,
            duration: row.duration,
            width: row.width as u32,
            height: row.height as u32,
            fps: row.fps,
            probe_json: row.probe_json,
            storage: StorageStatus {
                fs_video: row.fs_video,
                fs_json: row.fs_json,
                psql_chunk: row.psql_chunk,
                pobject_chunk: row.pobject_chunk,
                mobject_chunk: row.mobject_chunk,
                pvector_chunk: row.pvector_chunk,
                qvector_chunk: row.qvector_chunk,
            },
            status: VideoStatus::from_db_str(&row.status).unwrap_or(VideoStatus::Pending),
            user_id: row.user_id.map(|v| v as i64),
            job_id: row.job_id.map(|v| v as i64),
            created_at: row.created_at.unwrap_or_default(),
            registration_time: row.registration_time,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VideoRecord {
    pub id: i64,
    pub uuid: String,
    pub file_path: String,
    pub file_name: String,
    pub duration: f64,
    pub width: u32,
    pub height: u32,
    pub fps: f64,
    pub probe_json: Option<String>,
    pub storage: StorageStatus,
    pub status: VideoStatus,
    pub user_id: Option<i64>,
    pub job_id: Option<i64>,
    pub created_at: String,
    pub registration_time: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PreChunk {
    pub id: i64,
    pub file_id: i64,
    pub source_type: String,
    pub source_file: Option<String>,
    pub chunk_type: String,
    pub start_frame: i64,
    pub end_frame: i64,
    pub fps: f64,
    pub raw_json: serde_json::Value,
    pub text_content: Option<String>,
    pub processed: bool,
    pub chunk_id: Option<String>,
    pub created_at: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Frame {
    pub id: i64,
    pub file_id: i64,
    pub frame_number: i64,
    pub timestamp: f64,
    pub fps: f64,
    pub yolo_objects: Option<serde_json::Value>,
    pub ocr_results: Option<serde_json::Value>,
    pub face_results: Option<serde_json::Value>,
    pub pose_results: Option<serde_json::Value>,
    pub frame_path: Option<String>,
    pub created_at: String,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum MonitorJobStatus {
    Pending,
    Running,
    Completed,
    Failed,
    Cancelled,
}

impl MonitorJobStatus {
    pub fn as_str(&self) -> &'static str {
        match self {
            MonitorJobStatus::Pending => "pending",
            MonitorJobStatus::Running => "running",
            MonitorJobStatus::Completed => "completed",
            MonitorJobStatus::Failed => "failed",
            MonitorJobStatus::Cancelled => "cancelled",
        }
    }

    pub fn from_db_str(s: &str) -> Option<Self> {
        match s {
            "pending" => Some(MonitorJobStatus::Pending),
            "running" => Some(MonitorJobStatus::Running),
            "completed" => Some(MonitorJobStatus::Completed),
            "failed" => Some(MonitorJobStatus::Failed),
            "cancelled" => Some(MonitorJobStatus::Cancelled),
            _ => None,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MonitorJob {
    pub id: i32,
    pub uuid: String,
    pub video_path: Option<String>,
    pub status: MonitorJobStatus,
    pub current_processor: Option<String>,
    pub progress_total: i32,
    pub progress_current: i32,
    pub error_count: i32,
    pub last_error: Option<String>,
    pub started_at: Option<chrono::NaiveDateTime>,
    pub updated_at: Option<chrono::NaiveDateTime>,
    pub created_at: chrono::NaiveDateTime,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MonitorJobStats {
    pub pending: i32,
    pub running: i32,
    pub completed: i32,
    pub failed: i32,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum ProcessorType {
    Asr,
    Cut,
    Yolo,
    Ocr,
    Face,
    Pose,
    Asrx,
    VisualChunk,
}

impl ProcessorType {
    pub fn as_str(&self) -> &'static str {
        match self {
            ProcessorType::Asr => "asr",
            ProcessorType::Cut => "cut",
            ProcessorType::Yolo => "yolo",
            ProcessorType::Ocr => "ocr",
            ProcessorType::Face => "face",
            ProcessorType::Pose => "pose",
            ProcessorType::Asrx => "asrx",
            ProcessorType::VisualChunk => "visual_chunk",
        }
    }

    pub fn from_db_str(s: &str) -> Option<Self> {
        match s {
            "asr" => Some(ProcessorType::Asr),
            "cut" => Some(ProcessorType::Cut),
            "yolo" => Some(ProcessorType::Yolo),
            "ocr" => Some(ProcessorType::Ocr),
            "face" => Some(ProcessorType::Face),
            "pose" => Some(ProcessorType::Pose),
            "asrx" => Some(ProcessorType::Asrx),
            "visual_chunk" => Some(ProcessorType::VisualChunk),
            _ => None,
        }
    }

    pub fn all() -> Vec<ProcessorType> {
        vec![
            ProcessorType::Asr,
            ProcessorType::Cut,
            ProcessorType::Yolo,
            ProcessorType::Ocr,
            ProcessorType::Face,
            ProcessorType::Pose,
            ProcessorType::Asrx,
            ProcessorType::VisualChunk,
        ]
    }
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ProcessorJobStatus {
    Pending,
    Running,
    Completed,
    Failed,
    Skipped,
}

impl ProcessorJobStatus {
    pub fn as_str(&self) -> &'static str {
        match self {
            ProcessorJobStatus::Pending => "pending",
            ProcessorJobStatus::Running => "running",
            ProcessorJobStatus::Completed => "completed",
            ProcessorJobStatus::Failed => "failed",
            ProcessorJobStatus::Skipped => "skipped",
        }
    }

    pub fn from_db_str(s: &str) -> Option<Self> {
        match s {
            "pending" => Some(ProcessorJobStatus::Pending),
            "running" => Some(ProcessorJobStatus::Running),
            "completed" => Some(ProcessorJobStatus::Completed),
            "failed" => Some(ProcessorJobStatus::Failed),
            "skipped" => Some(ProcessorJobStatus::Skipped),
            _ => None,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProcessorResult {
    pub id: i32,
    pub job_id: i32,
    pub processor_type: ProcessorType,
    pub status: ProcessorJobStatus,
    pub started_at: Option<String>,
    pub completed_at: Option<String>,
    pub duration_secs: Option<f64>,
    pub error_message: Option<String>,
    pub output_data: Option<serde_json::Value>,
    pub retry_count: i32,
    pub created_at: String,
    pub updated_at: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct ApiKeyRecord {
    pub id: i32,
    pub key_id: String,
    pub key_hash: String,
    pub key_prefix: String,
    pub name: String,
    pub key_type: String,
    pub user_id: Option<i64>,
    pub service_name: Option<String>,
    pub permissions: serde_json::Value,
    pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
    pub last_used_at: Option<chrono::DateTime<chrono::Utc>>,
    pub last_used_ip: Option<String>,
    pub usage_count: i64,
    pub status: String,
    pub rotation_required: bool,
    pub rotation_reason: Option<String>,
    pub grace_period_end: Option<chrono::DateTime<chrono::Utc>>,
    pub created_at: chrono::DateTime<chrono::Utc>,
    pub updated_at: chrono::DateTime<chrono::Utc>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiKeyStats {
    pub total_keys: i64,
    pub active_keys: i64,
    pub expired_keys: i64,
    pub rotation_required: i64,
    pub anomalies_last_24h: i64,
}

#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct GiteaTokenRecord {
    pub id: i32,
    pub gitea_token_id: i64,
    pub gitea_user: String,
    pub token_name: String,
    pub token_last_eight: String,
    pub scopes: serde_json::Value,
    pub api_key_id: Option<String>,
    pub last_verified: Option<chrono::DateTime<chrono::Utc>>,
    pub created_at: chrono::DateTime<chrono::Utc>,
}

#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct N8nApiKeyRecord {
    pub id: i32,
    pub n8n_key_id: String,
    pub label: String,
    pub api_key_last_eight: String,
    pub momentry_api_key_id: Option<String>,
    pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
    pub last_verified: Option<chrono::DateTime<chrono::Utc>>,
    pub created_at: chrono::DateTime<chrono::Utc>,
}

/// Configuration for creating an API key
#[derive(Debug, Clone)]
pub struct CreateApiKeyConfig<'a> {
    pub key_id: &'a str,
    pub key_hash: &'a str,
    pub key_prefix: &'a str,
    pub name: &'a str,
    pub key_type: &'a str,
    pub user_id: Option<i64>,
    pub service_name: Option<&'a str>,
    pub permissions: &'a serde_json::Value,
    pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
}

impl<'a> CreateApiKeyConfig<'a> {
    pub fn new(
        key_id: &'a str,
        key_hash: &'a str,
        key_prefix: &'a str,
        name: &'a str,
        key_type: &'a str,
    ) -> Self {
        static DEFAULT_PERMISSIONS: std::sync::LazyLock<serde_json::Value> =
            std::sync::LazyLock::new(|| serde_json::json!(["read", "write"]));

        Self {
            key_id,
            key_hash,
            key_prefix,
            name,
            key_type,
            user_id: None,
            service_name: None,
            permissions: &DEFAULT_PERMISSIONS,
            expires_at: None,
        }
    }

    pub fn with_user_id(mut self, user_id: i64) -> Self {
        self.user_id = Some(user_id);
        self
    }

    pub fn with_service_name(mut self, name: &'a str) -> Self {
        self.service_name = Some(name);
        self
    }

    pub fn with_permissions(mut self, perms: &'a serde_json::Value) -> Self {
        self.permissions = perms;
        self
    }

    pub fn with_expires_at(mut self, expires: chrono::DateTime<chrono::Utc>) -> Self {
        self.expires_at = Some(expires);
        self
    }
}

#[derive(Clone)]
pub struct PostgresDb {
    pool: PgPool,
    cache: Arc<RwLock<PostgresCache>>,
}

#[derive(Debug, Default)]
pub struct PostgresCache {
    videos: std::collections::HashMap<String, VideoRecord>,
    #[allow(dead_code)]
    chunks: std::collections::HashMap<String, Vec<Chunk>>,
}

// Result structure for semantic search
#[derive(Debug, serde::Serialize, sqlx::FromRow)]
pub struct SemanticSearchResult {
    pub id: i32,
    pub scene_order: i32,
    pub start_time: f64,
    pub end_time: f64,
    pub summary: String,
    pub metadata: serde_json::Value,
    pub similarity: Option<f64>,
}

/// Result structure for child chunks
#[derive(Debug, serde::Serialize, sqlx::FromRow)]
pub struct ChildChunkResult {
    pub id: i32,
    pub parent_id: i32,

    // Frame Data
    pub start_frame: i64,
    pub end_frame: i64,
    pub fps: f64,

    // Time Data
    pub start_time: f64,
    pub end_time: f64,
    pub raw_text: String,
    pub speaker_ids: Vec<String>,
}

impl PostgresDb {
    pub async fn new(database_url: &str) -> Result<Self> {
        let max_connections = std::env::var("DB_MAX_CONNECTIONS")
            .unwrap_or_else(|_| "10".to_string())
            .parse::<u32>()
            .unwrap_or(10);

        let acquire_timeout_secs = std::env::var("DB_ACQUIRE_TIMEOUT")
            .unwrap_or_else(|_| "60".to_string())
            .parse::<u64>()
            .unwrap_or(60);

        tracing::info!(
            "DB pool config: max_connections={}, acquire_timeout={}s",
            max_connections,
            acquire_timeout_secs
        );

        let schema = crate::core::config::DATABASE_SCHEMA.as_str();
        tracing::info!("Database schema configuration: {}", schema);

        let pool_options = PgPoolOptions::new()
            .max_connections(max_connections)
            .acquire_timeout(std::time::Duration::from_secs(acquire_timeout_secs))
            .after_connect(move |conn, _| {
                let schema = schema.to_string();
                tracing::debug!("after_connect: setting search_path to {}", schema);
                Box::pin(async move {
                    // Always set search_path explicitly to avoid using default "dev, public"
                    sqlx::query(&format!("SET search_path TO {}", schema))
                        .execute(conn)
                        .await?;
                    Ok(())
                })
            });

        let pool = pool_options.connect(database_url).await?;

        if schema != "public" {
            tracing::info!("Creating PostgreSQL schema if not exists: {}", schema);
            sqlx::query(&format!("CREATE SCHEMA IF NOT EXISTS {}", schema))
                .execute(&pool)
                .await?;
        }

        let db = Self {
            pool,
            cache: Arc::new(RwLock::new(PostgresCache::default())),
        };

        // Migrations are now handled externally or via run_migrations()
        // db.init_schema().await?;

        Ok(db)
    }

    /// Run database migrations (Schema initialization)
    pub async fn run_migrations(pool: &PgPool) -> Result<()> {
        tracing::info!("Running database migrations...");

        // Create schema if not exists
        let schema = crate::core::config::DATABASE_SCHEMA.as_str();
        if schema != "public" {
            sqlx::query(&format!("CREATE SCHEMA IF NOT EXISTS {}", schema))
                .execute(pool)
                .await?;
        }

        // Videos
        sqlx::query("CREATE TABLE IF NOT EXISTS videos (id SERIAL PRIMARY KEY, uuid VARCHAR(32) UNIQUE NOT NULL, file_path TEXT NOT NULL, file_name TEXT NOT NULL, duration DOUBLE PRECISION, width INTEGER, height INTEGER, fps DOUBLE PRECISION, probe_json TEXT, fs_video BOOLEAN DEFAULT FALSE, fs_json BOOLEAN DEFAULT FALSE, psql_chunk BOOLEAN DEFAULT FALSE, pobject_chunk BOOLEAN DEFAULT FALSE, mobject_chunk BOOLEAN DEFAULT FALSE, pvector_chunk BOOLEAN DEFAULT FALSE, qvector_chunk BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)").execute(pool).await?;
        sqlx::query("CREATE INDEX IF NOT EXISTS idx_videos_uuid ON videos(uuid)")
            .execute(pool)
            .await?;

        // Chunks
        sqlx::query("CREATE TABLE IF NOT EXISTS chunks (id SERIAL PRIMARY KEY, uuid VARCHAR(32) NOT NULL, chunk_id VARCHAR(64) NOT NULL, chunk_index INTEGER NOT NULL, chunk_type VARCHAR(32) NOT NULL, start_time DOUBLE PRECISION NOT NULL, end_time DOUBLE PRECISION NOT NULL, fps DOUBLE PRECISION DEFAULT 24.0, start_frame BIGINT DEFAULT 0, end_frame BIGINT DEFAULT 0, content JSONB NOT NULL, metadata JSONB, vector_id VARCHAR(64), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(uuid, chunk_id))").execute(pool).await?;
        sqlx::query("CREATE INDEX IF NOT EXISTS idx_chunks_uuid ON chunks(uuid)")
            .execute(pool)
            .await?;
        sqlx::query("CREATE INDEX IF NOT EXISTS idx_chunks_type ON chunks(chunk_type)")
            .execute(pool)
            .await?;

        // Monitor Jobs
        sqlx::query("CREATE TABLE IF NOT EXISTS monitor_jobs (id SERIAL PRIMARY KEY, uuid VARCHAR(16) NOT NULL, video_path VARCHAR(512), status VARCHAR(20) NOT NULL DEFAULT 'pending', current_processor VARCHAR(20), progress_total INT DEFAULT 0, progress_current INT DEFAULT 0, error_count INT DEFAULT 0, last_error TEXT, started_at TIMESTAMP, updated_at TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)").execute(pool).await?;
        sqlx::query("CREATE INDEX IF NOT EXISTS idx_monitor_jobs_status ON monitor_jobs(status)")
            .execute(pool)
            .await?;

        // Processor Results
        sqlx::query("CREATE TABLE IF NOT EXISTS processor_results (id SERIAL PRIMARY KEY, job_id INTEGER, video_uuid VARCHAR(255) NOT NULL, processor VARCHAR(64), processor_type VARCHAR(64) NOT NULL, status VARCHAR(32) DEFAULT 'pending', result JSONB, error_message TEXT, started_at TIMESTAMP WITH TIME ZONE, completed_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP)").execute(pool).await?;

        // Talents & Identity Bindings
        sqlx::query("CREATE TABLE IF NOT EXISTS talents (id BIGSERIAL PRIMARY KEY, real_name VARCHAR(255) NOT NULL UNIQUE, actor_name VARCHAR(255), voice_embedding TEXT, face_embedding TEXT, metadata JSONB DEFAULT '{}', created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP)").execute(pool).await?;
        sqlx::query("CREATE TABLE IF NOT EXISTS identity_bindings (id BIGSERIAL PRIMARY KEY, identity_id BIGINT REFERENCES talents(id) ON DELETE CASCADE, identity_type VARCHAR(20) NOT NULL, identity_value VARCHAR(100) NOT NULL, metadata JSONB DEFAULT '{}', confidence DOUBLE PRECISION DEFAULT 1.0, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, UNIQUE(identity_id, identity_type, identity_value))").execute(pool).await?;

        // API Keys
        sqlx::query("CREATE TABLE IF NOT EXISTS api_keys (id SERIAL PRIMARY KEY, key_id VARCHAR(48) UNIQUE NOT NULL, key_hash VARCHAR(64) NOT NULL, key_prefix VARCHAR(8) NOT NULL, name VARCHAR(128) NOT NULL, key_type VARCHAR(20) NOT NULL DEFAULT 'user', user_id BIGINT, service_name VARCHAR(64), permissions JSONB DEFAULT '[\"read\", \"write\"]', expires_at TIMESTAMP, last_used_at TIMESTAMP, last_used_ip VARCHAR(45), usage_count BIGINT DEFAULT 0, status VARCHAR(20) NOT NULL DEFAULT 'active', rotation_required BOOLEAN DEFAULT FALSE, rotation_reason TEXT, grace_period_end TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)").execute(pool).await?;
        sqlx::query("CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash)")
            .execute(pool)
            .await?;

        // API Key Audit Log
        sqlx::query("CREATE TABLE IF NOT EXISTS api_key_audit_log (id SERIAL PRIMARY KEY, key_id VARCHAR(32) NOT NULL, action VARCHAR(50) NOT NULL, actor VARCHAR(128), ip_address VARCHAR(45), user_agent TEXT, request_path TEXT, response_code INT, anomaly_type VARCHAR(30), details JSONB, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)").execute(pool).await?;

        // API Key Anomalies
        sqlx::query("CREATE TABLE IF NOT EXISTS api_key_anomalies (id SERIAL PRIMARY KEY, key_id VARCHAR(32) NOT NULL, anomaly_type VARCHAR(30) NOT NULL, severity VARCHAR(10) NOT NULL, ip_address VARCHAR(45), request_count INT, error_count INT, error_rate DOUBLE PRECISION, unique_ips INT, details JSONB, resolved BOOLEAN DEFAULT FALSE, resolved_at TIMESTAMP, resolved_by VARCHAR(128), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)").execute(pool).await?;

        // Gitea Tokens
        sqlx::query("CREATE TABLE IF NOT EXISTS gitea_tokens (id SERIAL PRIMARY KEY, gitea_token_id BIGINT NOT NULL, gitea_user VARCHAR(128) NOT NULL, token_name VARCHAR(128) NOT NULL, token_last_eight VARCHAR(8) NOT NULL, scopes JSONB DEFAULT '[]', api_key_id VARCHAR(48), last_verified TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(gitea_user, token_name))").execute(pool).await?;

        // N8N API Keys
        sqlx::query("CREATE TABLE IF NOT EXISTS n8n_api_keys (id SERIAL PRIMARY KEY, n8n_key_id VARCHAR(64) UNIQUE NOT NULL, label VARCHAR(100) NOT NULL, api_key_last_eight VARCHAR(8) NOT NULL, momentry_api_key_id VARCHAR(48), expires_at TIMESTAMP WITH TIME ZONE, last_verified TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP)").execute(pool).await?;

        // Search Trigger
        sqlx::query(
            "CREATE OR REPLACE FUNCTION update_search_vector() RETURNS TRIGGER AS $func$
            BEGIN
                NEW.search_vector := to_tsvector('english', COALESCE(NEW.text_content, ''));
                RETURN NEW;
            END;
            $func$ LANGUAGE plpgsql",
        )
        .execute(pool)
        .await?;

        sqlx::query("DROP TRIGGER IF EXISTS chunks_search_vector_trigger ON chunks")
            .execute(pool)
            .await?;

        sqlx::query(
            "CREATE TRIGGER chunks_search_vector_trigger
            BEFORE INSERT OR UPDATE ON chunks
            FOR EACH ROW EXECUTE FUNCTION update_search_vector()",
        )
        .execute(pool)
        .await?;

        // Chunks Rule 1
        sqlx::query("CREATE TABLE IF NOT EXISTS chunks_rule1 (id UUID PRIMARY KEY DEFAULT gen_random_uuid(), asset_uuid VARCHAR(32) NOT NULL REFERENCES videos(uuid) ON DELETE CASCADE, start_frame BIGINT NOT NULL, end_frame BIGINT NOT NULL, content TEXT NOT NULL, speaker_id VARCHAR(50), created_at TIMESTAMPTZ DEFAULT NOW())").execute(pool).await?;
        sqlx::query(
            "CREATE INDEX IF NOT EXISTS idx_chunks_rule1_asset ON chunks_rule1(asset_uuid)",
        )
        .execute(pool)
        .await?;

        // Jobs (Legacy/P0)
        sqlx::query("CREATE TABLE IF NOT EXISTS jobs (id UUID PRIMARY KEY, asset_uuid VARCHAR(32) NOT NULL REFERENCES videos(uuid) ON DELETE CASCADE, processor_list TEXT[], assigned_processor_id UUID, rule VARCHAR(20), status VARCHAR(20) DEFAULT 'QUEUED', total_frames BIGINT DEFAULT 0, processed_frames BIGINT DEFAULT 0, error_message TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW())").execute(pool).await?;
        sqlx::query("CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status)")
            .execute(pool)
            .await?;

        tracing::info!("Database migrations completed.");
        Ok(())
    }

    /// Get a reference to the connection pool
    pub fn pool(&self) -> &PgPool {
        &self.pool
    }

    pub async fn register_video(&self, record: &VideoRecord) -> Result<i64> {
        let table = schema::table_name("videos");
        let result = sqlx::query(
            &format!(
                r#"
                INSERT INTO {} (uuid, file_path, file_name, duration, width, height, fps, probe_json, fs_video, status, user_id, job_id)
                VALUES ($1, $2, $3, $4, $5, $6, $7, $8, TRUE, $9, $10, $11)
                ON CONFLICT (uuid) DO UPDATE SET
                    file_path = EXCLUDED.file_path,
                    file_name = EXCLUDED.file_name,
                    duration = EXCLUDED.duration,
                    width = EXCLUDED.width,
                    height = EXCLUDED.height,
                    fps = EXCLUDED.fps,
                    probe_json = EXCLUDED.probe_json,
                    fs_video = TRUE,
                    status = COALESCE(EXCLUDED.status, {}.status),
                    user_id = COALESCE(EXCLUDED.user_id, {}.user_id),
                    job_id = COALESCE(EXCLUDED.job_id, {}.job_id),
                    updated_at = CURRENT_TIMESTAMP
                RETURNING id::bigint
                "#,
                table, table, table, table
            )
        )
        .bind(&record.uuid)
        .bind(&record.file_path)
        .bind(&record.file_name)
        .bind(record.duration)
        .bind(record.width as i32)
        .bind(record.height as i32)
        .bind(record.fps)
        .bind(&record.probe_json)
        .bind(record.status.as_str())
        .bind(record.user_id)
        .bind(record.job_id)
        .fetch_one(&self.pool)
        .await?;

        let id: i64 = result.get(0);

        // Update cache
        let mut cache = self.cache.write().await;
        let mut record = record.clone();
        record.id = id as i64;
        cache.videos.insert(record.uuid.clone(), record);

        Ok(id)
    }

    pub async fn get_video_by_uuid(&self, uuid: &str) -> Result<Option<VideoRecord>> {
        // Check cache first
        {
            let cache = self.cache.read().await;
            if let Some(video) = cache.videos.get(uuid) {
                return Ok(Some(video.clone()));
            }
        }

        let table = schema::table_name("videos");
        let result = sqlx::query_as::<_, VideoRow>(
            &format!(
                "SELECT id, uuid, file_path, file_name, duration, width, height, fps, probe_json, fs_video, fs_json, psql_chunk, pobject_chunk, mobject_chunk, pvector_chunk, qvector_chunk, status, user_id, job_id, created_at::text, registration_time::text FROM {} WHERE uuid = $1",
                table
            )
        )
        .bind(uuid)
        .fetch_optional(&self.pool)
        .await?;

        if let Some(row) = result {
            let video: VideoRecord = row.into();

            // Update cache
            let mut cache = self.cache.write().await;
            cache.videos.insert(uuid.to_string(), video.clone());

            Ok(Some(video))
        } else {
            Ok(None)
        }
    }

    pub async fn get_videos_by_uuids(&self, uuids: &[String]) -> Result<Vec<VideoRecord>> {
        if uuids.is_empty() {
            return Ok(vec![]);
        }

        fn cached_values_to_vec(
            mut map: std::collections::HashMap<String, VideoRecord>,
            order: &[String],
        ) -> Vec<VideoRecord> {
            let mut results = Vec::with_capacity(order.len());
            for uuid in order {
                if let Some(video) = map.remove(uuid) {
                    results.push(video);
                }
            }
            results
        }

        // Check cache first and collect missing uuids
        let mut missing_uuids = Vec::new();
        let mut cached_results: std::collections::HashMap<String, VideoRecord> =
            std::collections::HashMap::new();

        {
            let cache = self.cache.read().await;
            for uuid in uuids {
                if let Some(video) = cache.videos.get(uuid) {
                    cached_results.insert(uuid.clone(), video.clone());
                } else {
                    missing_uuids.push(uuid.clone());
                }
            }
        }

        if missing_uuids.is_empty() {
            let mut results: Vec<VideoRecord> = cached_values_to_vec(cached_results, uuids);
            return Ok(results);
        }

        // Batch query for missing uuids
        let placeholders: Vec<String> = missing_uuids
            .iter()
            .enumerate()
            .map(|(i, _)| format!("${}", i + 1))
            .collect();
        let table = schema::table_name("videos");
        let sql = format!(
            "SELECT id, uuid, file_path, file_name, duration, width, height, fps, probe_json, fs_video, fs_json, psql_chunk, pobject_chunk, mobject_chunk, pvector_chunk, qvector_chunk, status, user_id, job_id FROM {} WHERE uuid = ANY($1)",
            table
        );

        let rows = sqlx::query_as::<_, VideoRow>(&sql)
            .bind(&missing_uuids)
            .fetch_all(&self.pool)
            .await?;

        let mut videos: std::collections::HashMap<String, VideoRecord> =
            std::collections::HashMap::new();
        for row in rows.into_iter() {
            let video: VideoRecord = row.into();
            videos.insert(video.uuid.clone(), video.clone());
            // Update cache
            let mut cache = self.cache.write().await;
            cache.videos.insert(video.uuid.clone(), video);
        }

        // Merge cached and fetched results
        cached_results.extend(videos);
        let mut results: Vec<VideoRecord> = cached_values_to_vec(cached_results, uuids);

        Ok(results)
    }

    pub async fn list_videos(&self, limit: i32, offset: i64) -> Result<(Vec<VideoRecord>, i64)> {
        // Default to unprocessed (status != 'ready')
        self.search_videos(None, Some(false), limit, offset).await
    }

    pub async fn search_videos(
        &self,
        query: Option<&str>,
        is_processed: Option<bool>,
        limit: i32,
        offset: i64,
    ) -> Result<(Vec<VideoRecord>, i64)> {
        let table = schema::table_name("videos");
        
        // Build status condition
        // is_processed = Some(true)  => status = 'ready'
        // is_processed = Some(false) => status != 'ready'
        // is_processed = None        => no filter
        let status_cond = match is_processed {
            Some(true) => "AND status = 'ready'",
            Some(false) => "AND status != 'ready'",
            None => "",
        };

        // Build search condition safely
        // If query is Some, we filter by filename/path/probe_json
        let search_cond = if query.is_some() {
            "AND (LOWER(file_name) LIKE $1 OR LOWER(file_path) LIKE $1 OR LOWER(probe_json::text) LIKE $1)"
        } else {
            ""
        };

        let where_clause = format!("WHERE 1=1 {} {}", status_cond, search_cond);

        // 1. Count Query
        // If query is present, $1 is the pattern.
        // If query is None, no pattern param needed for count? 
        // Actually, to keep code simple, let's just construct the query string.
        // SQLx query_as requires bind count to match placeholders.
        
        let count_query = format!("SELECT COUNT(*) FROM {} {}", table, where_clause);
        
        let total: i64 = if let Some(q) = query {
            let pattern = format!("%{}%", q.to_lowercase());
            sqlx::query_scalar(&count_query)
                .bind(&pattern)
                .fetch_one(&self.pool)
                .await?
        } else {
            sqlx::query_scalar(&count_query)
                .fetch_one(&self.pool)
                .await?
        };

        // 2. Select Query
        // Cast created_at and registration_time to text
        let columns = "id, uuid, file_path, file_name, duration, width, height, fps, probe_json, fs_video, fs_json, psql_chunk, pobject_chunk, mobject_chunk, pvector_chunk, qvector_chunk, status, user_id, job_id, created_at::text, registration_time::text";
        
        // Determine parameter order for LIMIT/OFFSET
        // If search is present, pattern is $1. Limit is $2. Offset is $3.
        // If search is not present, Limit is $1. Offset is $2.
        
        let select_query = if query.is_some() {
            format!("SELECT {} FROM {} {} ORDER BY id DESC LIMIT $2 OFFSET $3", columns, table, where_clause)
        } else {
            format!("SELECT {} FROM {} {} ORDER BY id DESC LIMIT $1 OFFSET $2", columns, table, where_clause)
        };

        let rows = if let Some(q) = query {
            let pattern = format!("%{}%", q.to_lowercase());
            sqlx::query_as::<_, VideoRow>(&select_query)
                .bind(&pattern)
                .bind(limit)
                .bind(offset)
                .fetch_all(&self.pool)
                .await?
        } else {
            sqlx::query_as::<_, VideoRow>(&select_query)
                .bind(limit)
                .bind(offset)
                .fetch_all(&self.pool)
                .await?
        };

        let videos: Vec<VideoRecord> = rows.into_iter().map(|r| r.into()).collect();
        Ok((videos, total))
    }

    pub async fn update_storage_status(&self, uuid: &str, field: &str, value: bool) -> Result<()> {
        let column = match field {
            "fs_video" => "fs_video",
            "fs_json" => "fs_json",
            "psql_chunk" => "psql_chunk",
            "pobject_chunk" => "pobject_chunk",
            "mobject_chunk" => "mobject_chunk",
            "pvector_chunk" => "pvector_chunk",
            "qvector_chunk" => "qvector_chunk",
            _ => return Err(anyhow::anyhow!("Invalid storage field: {}", field)),
        };

        let table = schema::table_name("videos");
        sqlx::query(&format!(
            "UPDATE {} SET {} = $1, updated_at = CURRENT_TIMESTAMP WHERE uuid = $2",
            table, column
        ))
        .bind(value)
        .bind(uuid)
        .execute(&self.pool)
        .await?;

        // Invalidate cache
        let mut cache = self.cache.write().await;
        cache.videos.remove(uuid);

        Ok(())
    }

    pub async fn set_registration_time(&self, uuid: &str) -> Result<()> {
        let table = schema::table_name("videos");
        sqlx::query(&format!(
            "UPDATE {} SET registration_time = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE uuid = $1 AND registration_time IS NULL",
            table
        ))
        .bind(uuid)
        .execute(&self.pool)
        .await?;

        Ok(())
    }

    pub async fn delete_video(&self, uuid: &str) -> Result<()> {
        tracing::info!("[PostgresDb] Deleting video: {}", uuid);

        let tx = self.pool.begin().await?;

        let chunk_vectors = schema::table_name("chunk_vectors");
        let chunks = schema::table_name("chunks");
        let processor_results = schema::table_name("processor_results");
        let videos = schema::table_name("videos");

        sqlx::query(&format!("DELETE FROM {} WHERE uuid = $1", chunk_vectors))
            .bind(uuid)
            .execute(&self.pool)
            .await?;

        sqlx::query(&format!("DELETE FROM {} WHERE uuid = $1", chunks))
            .bind(uuid)
            .execute(&self.pool)
            .await?;

        sqlx::query(&format!(
            "DELETE FROM {} WHERE video_id IN (SELECT id FROM {} WHERE uuid = $1)",
            processor_results, videos
        ))
        .bind(uuid)
        .execute(&self.pool)
        .await?;

        sqlx::query(&format!("DELETE FROM {} WHERE uuid = $1", videos))
            .bind(uuid)
            .execute(&self.pool)
            .await?;

        tx.commit().await?;

        let mut cache = self.cache.write().await;
        cache.videos.remove(uuid);

        tracing::info!("[PostgresDb] Video deleted: {}", uuid);
        Ok(())
    }

    pub async fn get_storage_status(&self, uuid: &str) -> Result<Option<StorageStatus>> {
        if let Some(video) = self.get_video_by_uuid(uuid).await? {
            Ok(Some(video.storage))
        } else {
            Ok(None)
        }
    }

    pub async fn get_chunk_count(&self, uuid: &str) -> Result<(i64, i64)> {
        let chunks = schema::table_name("chunks");
        let sentence_count: i64 = sqlx::query_scalar(&format!(
            "SELECT COUNT(*) FROM {} WHERE uuid = $1 AND chunk_type = 'sentence'",
            chunks
        ))
        .bind(uuid)
        .fetch_one(&self.pool)
        .await?;

        let time_count: i64 = sqlx::query_scalar(&format!(
            "SELECT COUNT(*) FROM {} WHERE uuid = $1 AND chunk_type = 'time_based'",
            chunks
        ))
        .bind(uuid)
        .fetch_one(&self.pool)
        .await?;

        Ok((sentence_count, time_count))
    }

    pub async fn get_vector_count(&self, uuid: &str) -> Result<i64> {
        let chunk_vectors = schema::table_name("chunk_vectors");
        let count: i64 = sqlx::query_scalar(&format!(
            "SELECT COUNT(*) FROM {} WHERE uuid = $1",
            chunk_vectors
        ))
        .bind(uuid)
        .fetch_one(&self.pool)
        .await?;

        Ok(count)
    }

    pub async fn create_monitor_job(
        &self,
        uuid: &str,
        video_path: Option<&str>,
    ) -> Result<MonitorJob> {
        let table = schema::table_name("monitor_jobs");
        let row = sqlx::query(
            &format!(
                r#"
                INSERT INTO {} (uuid, video_path, status)
                VALUES ($1, $2, 'pending')
                ON CONFLICT DO NOTHING
                RETURNING id, uuid, video_path, status, current_processor, progress_total, progress_current, error_count, last_error, started_at, updated_at, created_at
                "#,
                table
            )
        )
        .bind(uuid)
        .bind(video_path)
        .fetch_one(&self.pool)
        .await?;

        let status_str: String = row.get(3);
        let status =
            MonitorJobStatus::from_db_str(&status_str).unwrap_or(MonitorJobStatus::Pending);

        Ok(MonitorJob {
            id: row.get(0),
            uuid: row.get(1),
            video_path: row.get(2),
            status,
            current_processor: row.get(4),
            progress_total: row.get(5),
            progress_current: row.get(6),
            error_count: row.get(7),
            last_error: row.get(8),
            started_at: row.get(9),
            updated_at: row.get(10),
            created_at: row.get(11),
        })
    }

    pub async fn update_video_job_id(&self, video_id: i64, job_id: i32) -> Result<()> {
        let videos = schema::table_name("videos");
        sqlx::query(&format!(
            r#"
                UPDATE {} 
                SET job_id = $1, updated_at = CURRENT_TIMESTAMP 
                WHERE id = $2
                "#,
            videos
        ))
        .bind(job_id)
        .bind(video_id)
        .execute(&self.pool)
        .await?;

        Ok(())
    }

    pub async fn update_monitor_job_video_id(&self, job_id: i32, video_id: i64) -> Result<()> {
        let monitor_jobs = schema::table_name("monitor_jobs");
        sqlx::query(&format!(
            r#"
                UPDATE {} 
                SET video_id = $1, updated_at = CURRENT_TIMESTAMP 
                WHERE id = $2
                "#,
            monitor_jobs
        ))
        .bind(video_id)
        .bind(job_id)
        .execute(&self.pool)
        .await?;

        Ok(())
    }

    pub async fn get_monitor_job_by_uuid(&self, uuid: &str) -> Result<Option<MonitorJob>> {
        let table = schema::table_name("monitor_jobs");
        let row = sqlx::query(
            &format!(
                r#"
                SELECT id, uuid, video_path, status, current_processor, progress_total, progress_current, error_count, last_error, started_at, updated_at, created_at
                FROM {} WHERE uuid = $1
                "#,
                table
            )
        )
        .bind(uuid)
        .fetch_optional(&self.pool)
        .await?;

        if let Some(r) = row {
            let status_str: String = r.get(3);
            let status =
                MonitorJobStatus::from_db_str(&status_str).unwrap_or(MonitorJobStatus::Pending);

            Ok(Some(MonitorJob {
                id: r.get(0),
                uuid: r.get(1),
                video_path: r.get(2),
                status,
                current_processor: r.get(4),
                progress_total: r.get(5),
                progress_current: r.get(6),
                error_count: r.get(7),
                last_error: r.get(8),
                started_at: r.get(9),
                updated_at: r.get(10),
                created_at: r.get(11),
            }))
        } else {
            Ok(None)
        }
    }

    pub async fn list_monitor_jobs_by_status(
        &self,
        status: MonitorJobStatus,
    ) -> Result<Vec<MonitorJob>> {
        let table = schema::table_name("monitor_jobs");
        let rows = sqlx::query(
            &format!(
                r#"
                SELECT id, uuid, video_path, status, current_processor, progress_total, progress_current, error_count, last_error, started_at, updated_at, created_at
                FROM {} WHERE status = $1 ORDER BY created_at DESC
                "#,
                table
            )
        )
        .bind(status.as_str())
        .fetch_all(&self.pool)
        .await?;

        let jobs: Vec<MonitorJob> = rows
            .into_iter()
            .map(|r| {
                let status_str: String = r.get(3);
                let status =
                    MonitorJobStatus::from_db_str(&status_str).unwrap_or(MonitorJobStatus::Pending);

                MonitorJob {
                    id: r.get(0),
                    uuid: r.get(1),
                    video_path: r.get(2),
                    status,
                    current_processor: r.get(4),
                    progress_total: r.get(5),
                    progress_current: r.get(6),
                    error_count: r.get(7),
                    last_error: r.get(8),
                    started_at: r.get(9),
                    updated_at: r.get(10),
                    created_at: r.get(11),
                }
            })
            .collect();

        Ok(jobs)
    }

    pub async fn update_monitor_job_status(
        &self,
        uuid: &str,
        status: MonitorJobStatus,
    ) -> Result<()> {
        let started_at_clause = if status == MonitorJobStatus::Running {
            ", started_at = COALESCE(started_at, CURRENT_TIMESTAMP)"
        } else {
            ""
        };

        let table = schema::table_name("monitor_jobs");
        sqlx::query(&format!(
            "UPDATE {} SET status = $1, updated_at = CURRENT_TIMESTAMP{} WHERE uuid = $2",
            table, started_at_clause
        ))
        .bind(status.as_str())
        .bind(uuid)
        .execute(&self.pool)
        .await?;

        Ok(())
    }

    pub async fn update_monitor_job_progress(
        &self,
        uuid: &str,
        current_processor: Option<&str>,
        progress_current: i32,
        progress_total: i32,
    ) -> Result<()> {
        let table = schema::table_name("monitor_jobs");
        sqlx::query(&format!(
            r#"
                UPDATE {} 
                SET current_processor = COALESCE($3, current_processor),
                    progress_current = $4,
                    progress_total = $5,
                    updated_at = CURRENT_TIMESTAMP
                WHERE uuid = $1
                "#,
            table
        ))
        .bind(uuid)
        .bind(uuid)
        .bind(current_processor)
        .bind(progress_current)
        .bind(progress_total)
        .execute(&self.pool)
        .await?;

        Ok(())
    }

    pub async fn update_monitor_job_error(&self, uuid: &str, error: &str) -> Result<()> {
        let table = schema::table_name("monitor_jobs");
        sqlx::query(&format!(
            r#"
                UPDATE {} 
                SET error_count = error_count + 1,
                    last_error = $2,
                    updated_at = CURRENT_TIMESTAMP
                WHERE uuid = $1
                "#,
            table
        ))
        .bind(uuid)
        .bind(error)
        .execute(&self.pool)
        .await?;

        Ok(())
    }

    pub async fn delete_monitor_job(&self, uuid: &str) -> Result<bool> {
        let table = schema::table_name("monitor_jobs");
        let result = sqlx::query(&format!("DELETE FROM {} WHERE uuid = $1", table))
            .bind(uuid)
            .execute(&self.pool)
            .await?;

        Ok(result.rows_affected() > 0)
    }

    pub async fn get_monitor_job_stats(&self) -> Result<MonitorJobStats> {
        let table = schema::table_name("monitor_jobs");
        let pending: i64 = sqlx::query_scalar(&format!(
            "SELECT COUNT(*) FROM {} WHERE status = 'pending'",
            table
        ))
        .fetch_one(&self.pool)
        .await?;

        let running: i64 = sqlx::query_scalar(&format!(
            "SELECT COUNT(*) FROM {} WHERE status = 'running'",
            table
        ))
        .fetch_one(&self.pool)
        .await?;

        let completed: i64 = sqlx::query_scalar(&format!(
            "SELECT COUNT(*) FROM {} WHERE status = 'completed'",
            table
        ))
        .fetch_one(&self.pool)
        .await?;

        let failed: i64 = sqlx::query_scalar(&format!(
            "SELECT COUNT(*) FROM {} WHERE status = 'failed'",
            table
        ))
        .fetch_one(&self.pool)
        .await?;

        Ok(MonitorJobStats {
            pending: pending as i32,
            running: running as i32,
            completed: completed as i32,
            failed: failed as i32,
        })
    }

    pub async fn create_api_key(&self, config: CreateApiKeyConfig<'_>) -> Result<i64> {
        let table = schema::table_name("api_keys");
        let result = sqlx::query(
            &format!(
                r#"
                INSERT INTO {} (key_id, key_hash, key_prefix, name, key_type, user_id, service_name, permissions, expires_at)
                VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9)
                RETURNING id
                "#,
                table
            ),
        )
        .bind(config.key_id)
        .bind(config.key_hash)
        .bind(config.key_prefix)
        .bind(config.name)
        .bind(config.key_type)
        .bind(config.user_id)
        .bind(config.service_name)
        .bind(config.permissions)
        .bind(config.expires_at)
        .fetch_one(&self.pool)
        .await?;

        let id: i32 = result.get(0);
        Ok(id as i64)
    }

    pub async fn get_api_key_by_hash(&self, key_hash: &str) -> Result<Option<ApiKeyRecord>> {
        let table = schema::table_name("api_keys");
        let result = sqlx::query_as::<_, ApiKeyRecord>(&format!(
            r#"
                SELECT id, key_id, key_hash, key_prefix, name, key_type, user_id, service_name, 
                       permissions, expires_at, last_used_at, last_used_ip, usage_count, status,
                       rotation_required, rotation_reason, grace_period_end, created_at, updated_at
                FROM {} WHERE key_hash = $1
                "#,
            table
        ))
        .bind(key_hash)
        .fetch_optional(&self.pool)
        .await?;

        Ok(result)
    }

    pub async fn get_api_key_by_key_id(&self, key_id: &str) -> Result<Option<ApiKeyRecord>> {
        let table = schema::table_name("api_keys");
        let result = sqlx::query_as::<_, ApiKeyRecord>(&format!(
            r#"
                SELECT id, key_id, key_hash, key_prefix, name, key_type, user_id, service_name,
                       permissions, expires_at, last_used_at, last_used_ip, usage_count, status,
                       rotation_required, rotation_reason, grace_period_end, created_at, updated_at
                FROM {} WHERE key_id = $1
                "#,
            table
        ))
        .bind(key_id)
        .fetch_optional(&self.pool)
        .await?;

        Ok(result)
    }

    pub async fn list_api_keys(&self) -> Result<Vec<ApiKeyRecord>> {
        let table = schema::table_name("api_keys");
        let results = sqlx::query_as::<_, ApiKeyRecord>(&format!(
            r#"
                SELECT id, key_id, key_hash, key_prefix, name, key_type, user_id, service_name,
                       permissions, expires_at, last_used_at, last_used_ip, usage_count, status,
                       rotation_required, rotation_reason, grace_period_end, created_at, updated_at
                FROM {} ORDER BY created_at DESC
                "#,
            table
        ))
        .fetch_all(&self.pool)
        .await?;

        Ok(results)
    }

    pub async fn update_api_key_usage(&self, key_id: &str, ip_address: Option<&str>) -> Result<()> {
        let table = schema::table_name("api_keys");
        sqlx::query(&format!(
            r#"
                UPDATE {} 
                SET last_used_at = CURRENT_TIMESTAMP,
                    last_used_ip = COALESCE($2, last_used_ip),
                    usage_count = usage_count + 1,
                    updated_at = CURRENT_TIMESTAMP
                WHERE key_id = $1
                "#,
            table
        ))
        .bind(key_id)
        .bind(ip_address)
        .execute(&self.pool)
        .await?;

        Ok(())
    }

    pub async fn require_api_key_rotation(
        &self,
        key_id: &str,
        reason: &str,
        grace_period_end: chrono::DateTime<chrono::Utc>,
    ) -> Result<()> {
        let table = schema::table_name("api_keys");
        sqlx::query(&format!(
            r#"
                UPDATE {} 
                SET rotation_required = TRUE,
                    rotation_reason = $2,
                    grace_period_end = $3,
                    updated_at = CURRENT_TIMESTAMP
                WHERE key_id = $1
                "#,
            table
        ))
        .bind(key_id)
        .bind(reason)
        .bind(grace_period_end)
        .execute(&self.pool)
        .await?;

        Ok(())
    }

    pub async fn revoke_api_key(&self, key_id: &str) -> Result<()> {
        sqlx::query(
            r#"
            UPDATE api_keys 
            SET status = 'revoked',
                updated_at = CURRENT_TIMESTAMP
            WHERE key_id = $1
            "#,
        )
        .bind(key_id)
        .execute(&self.pool)
        .await?;

        Ok(())
    }

    #[allow(clippy::too_many_arguments)]
    pub async fn log_api_key_audit(
        &self,
        key_id: &str,
        action: &str,
        actor: Option<&str>,
        ip_address: Option<&str>,
        user_agent: Option<&str>,
        request_path: Option<&str>,
        response_code: Option<i32>,
        anomaly_type: Option<&str>,
        details: Option<&serde_json::Value>,
    ) -> Result<()> {
        sqlx::query(
            r#"
            INSERT INTO api_key_audit_log (key_id, action, actor, ip_address, user_agent, request_path, response_code, anomaly_type, details)
            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb)
            "#,
        )
        .bind(key_id)
        .bind(action)
        .bind(actor)
        .bind(ip_address)
        .bind(user_agent)
        .bind(request_path)
        .bind(response_code)
        .bind(anomaly_type)
        .bind(details)
        .execute(&self.pool)
        .await?;

        Ok(())
    }

    pub async fn get_api_key_stats(&self) -> Result<ApiKeyStats> {
        let total: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM api_keys")
            .fetch_one(&self.pool)
            .await?;

        let active: i64 =
            sqlx::query_scalar("SELECT COUNT(*) FROM api_keys WHERE status = 'active'")
                .fetch_one(&self.pool)
                .await?;

        let expired: i64 = sqlx::query_scalar(
            "SELECT COUNT(*) FROM api_keys WHERE expires_at < CURRENT_TIMESTAMP",
        )
        .fetch_one(&self.pool)
        .await?;

        let rotation_required: i64 =
            sqlx::query_scalar("SELECT COUNT(*) FROM api_keys WHERE rotation_required = TRUE")
                .fetch_one(&self.pool)
                .await?;

        let anomalies_24h: i64 = sqlx::query_scalar(
            "SELECT COUNT(*) FROM api_key_anomalies WHERE created_at > CURRENT_TIMESTAMP - INTERVAL '24 hours'",
        )
        .fetch_one(&self.pool)
        .await?;

        Ok(ApiKeyStats {
            total_keys: total,
            active_keys: active,
            expired_keys: expired,
            rotation_required,
            anomalies_last_24h: anomalies_24h,
        })
    }

    pub async fn create_gitea_token(
        &self,
        gitea_token_id: i64,
        gitea_user: &str,
        token_name: &str,
        token_last_eight: &str,
        scopes: &serde_json::Value,
        api_key_id: Option<&str>,
    ) -> Result<i64> {
        let result = sqlx::query(
            r#"
            INSERT INTO gitea_tokens (gitea_token_id, gitea_user, token_name, token_last_eight, scopes, api_key_id)
            VALUES ($1, $2, $3, $4, $5::jsonb, $6)
            RETURNING id
            "#,
        )
        .bind(gitea_token_id)
        .bind(gitea_user)
        .bind(token_name)
        .bind(token_last_eight)
        .bind(scopes)
        .bind(api_key_id)
        .fetch_one(&self.pool)
        .await?;

        let id: i32 = result.get(0);
        Ok(id as i64)
    }

    pub async fn get_gitea_tokens_by_user(
        &self,
        gitea_user: &str,
    ) -> Result<Vec<GiteaTokenRecord>> {
        let table = schema::table_name("gitea_tokens");
        let results = sqlx::query_as::<_, GiteaTokenRecord>(&format!(
            r#"
            SELECT id, gitea_token_id, gitea_user, token_name, token_last_eight, scopes, api_key_id, last_verified, created_at
            FROM {} WHERE gitea_user = $1 ORDER BY created_at DESC
            "#,
            table
        ))
        .bind(gitea_user)
        .fetch_all(&self.pool)
        .await?;

        Ok(results)
    }

    pub async fn get_gitea_token_by_name(
        &self,
        gitea_user: &str,
        token_name: &str,
    ) -> Result<Option<GiteaTokenRecord>> {
        let table = schema::table_name("gitea_tokens");
        let result = sqlx::query_as::<_, GiteaTokenRecord>(&format!(
            r#"
            SELECT id, gitea_token_id, gitea_user, token_name, token_last_eight, scopes, api_key_id, last_verified, created_at
            FROM {} WHERE gitea_user = $1 AND token_name = $2
            "#,
            table
        ))
        .bind(gitea_user)
        .bind(token_name)
        .fetch_optional(&self.pool)
        .await?;

        Ok(result)
    }

    pub async fn delete_gitea_token(&self, gitea_user: &str, token_name: &str) -> Result<()> {
        let table = schema::table_name("gitea_tokens");
        sqlx::query(&format!(
            "DELETE FROM {} WHERE gitea_user = $1 AND token_name = $2",
            table
        ))
        .bind(gitea_user)
        .bind(token_name)
        .execute(&self.pool)
        .await?;

        Ok(())
    }

    pub async fn update_gitea_token_verification(
        &self,
        gitea_user: &str,
        token_name: &str,
    ) -> Result<()> {
        let table = schema::table_name("gitea_tokens");
        sqlx::query(&format!(
            r#"
            UPDATE {} 
            SET last_verified = CURRENT_TIMESTAMP
            WHERE gitea_user = $1 AND token_name = $2
            "#,
            table
        ))
        .bind(gitea_user)
        .bind(token_name)
        .execute(&self.pool)
        .await?;

        Ok(())
    }

    pub async fn create_n8n_api_key(
        &self,
        n8n_key_id: &str,
        label: &str,
        api_key_last_eight: &str,
        momentry_api_key_id: Option<&str>,
        expires_at: Option<chrono::DateTime<chrono::Utc>>,
    ) -> Result<i64> {
        let table = schema::table_name("n8n_api_keys");
        let result = sqlx::query(&format!(
            r#"
            INSERT INTO {} (n8n_key_id, label, api_key_last_eight, momentry_api_key_id, expires_at)
            VALUES ($1, $2, $3, $4, $5)
            RETURNING id
            "#,
            table
        ))
        .bind(n8n_key_id)
        .bind(label)
        .bind(api_key_last_eight)
        .bind(momentry_api_key_id)
        .bind(expires_at)
        .fetch_one(&self.pool)
        .await?;

        let id: i32 = result.get(0);
        Ok(id as i64)
    }

    pub async fn get_n8n_api_keys(&self) -> Result<Vec<N8nApiKeyRecord>> {
        let table = schema::table_name("n8n_api_keys");
        let results = sqlx::query_as::<_, N8nApiKeyRecord>(&format!(
            r#"
            SELECT id, n8n_key_id, label, api_key_last_eight, momentry_api_key_id, expires_at, last_verified, created_at
            FROM {} ORDER BY created_at DESC
            "#,
            table
        ))
        .fetch_all(&self.pool)
        .await?;

        Ok(results)
    }

    pub async fn get_n8n_api_key_by_label(&self, label: &str) -> Result<Option<N8nApiKeyRecord>> {
        let table = schema::table_name("n8n_api_keys");
        let result = sqlx::query_as::<_, N8nApiKeyRecord>(&format!(
            r#"
            SELECT id, n8n_key_id, label, api_key_last_eight, momentry_api_key_id, expires_at, last_verified, created_at
            FROM {} WHERE label = $1
            "#,
            table
        ))
        .bind(label)
        .fetch_optional(&self.pool)
        .await?;

        Ok(result)
    }

    pub async fn delete_n8n_api_key(&self, label: &str) -> Result<()> {
        let table = schema::table_name("n8n_api_keys");
        sqlx::query(&format!("DELETE FROM {} WHERE label = $1", table))
            .bind(label)
            .execute(&self.pool)
            .await?;

        Ok(())
    }

    pub async fn update_n8n_api_key_verification(&self, label: &str) -> Result<()> {
        let table = schema::table_name("n8n_api_keys");
        sqlx::query(&format!(
            r#"
            UPDATE {} 
            SET last_verified = CURRENT_TIMESTAMP
            WHERE label = $1
            "#,
            table
        ))
        .bind(label)
        .execute(&self.pool)
        .await?;

        Ok(())
    }

    /// Store a raw pre-chunk from a processor (e.g., YOLO frame, Face detection).
    /// This replaces the old direct-to-chunks approach for trace data.
    pub async fn store_raw_pre_chunk(
        &self,
        file_uuid: &str,
        processor_type: &str,
        coordinate_index: i64,
        timestamp: Option<f64>,
        data: &serde_json::Value,
        identity_id: Option<Uuid>,
        confidence: Option<f64>,
    ) -> Result<()> {
        let table = schema::table_name("pre_chunks");
        let query = format!(
            r#"
            INSERT INTO {} (
                file_uuid, processor_type, coordinate_type, coordinate_index, 
                timestamp, data, identity_id, confidence
            ) VALUES ($1, $2, 'frame', $3, $4, $5, $6, $7)
            "#,
            table
        );

        sqlx::query(&query)
            .bind(file_uuid)
            .bind(processor_type)
            .bind(coordinate_index)
            .bind(timestamp)
            .bind(data)
            .bind(identity_id)
            .bind(confidence)
            .execute(self.pool())
            .await
            .map_err(|e| anyhow::anyhow!("Failed to store raw pre_chunk: {}", e))?;

        Ok(())
    }

    /// Batch store pre-chunks for better performance (e.g. bulk insert of frames).
    pub async fn store_raw_pre_chunks_batch(
        &self,
        file_uuid: &str,
        processor_type: &str,
        chunks: &Vec<(i64, Option<f64>, serde_json::Value, Option<Uuid>, Option<f64>)>,
    ) -> Result<()> {
        // For large batches, we can use a loop or copy. Here using loop for safety with pgvector types if any.
        // Note: A transaction is recommended for batch inserts.
        let mut tx = self.pool().begin().await?;
        let table = schema::table_name("pre_chunks");
        let query = format!(
            r#"
            INSERT INTO {} (
                file_uuid, processor_type, coordinate_type, coordinate_index, 
                timestamp, data, identity_id, confidence
            ) VALUES ($1, $2, 'frame', $3, $4, $5, $6, $7)
            "#,
            table
        );

        for (coord_idx, ts, data, id, conf) in chunks {
            sqlx::query(&query)
                .bind(file_uuid)
                .bind(processor_type)
                .bind(*coord_idx)
                .bind(*ts)
                .bind(data)
                .bind(*id)
                .bind(*conf)
                .execute(&mut *tx)
                .await?;
        }
        tx.commit().await?;
        Ok(())
    }

    pub async fn register_resource(&self, resource: ResourceRecord) -> Result<()> {
        sqlx::query(
            "INSERT INTO resources (resource_id, resource_type, category, capabilities, config, metadata, status, last_heartbeat)
             VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
             ON CONFLICT (resource_id) DO UPDATE SET
             resource_type = EXCLUDED.resource_type,
             category = EXCLUDED.category,
             capabilities = EXCLUDED.capabilities,
             config = EXCLUDED.config,
             metadata = EXCLUDED.metadata,
             status = EXCLUDED.status,
             last_heartbeat = NOW()"
        )
        .bind(resource.resource_id)
        .bind(resource.resource_type)
        .bind(resource.category)
        .bind(resource.capabilities)
        .bind(resource.config)
        .bind(resource.metadata)
        .bind(resource.status)
        .execute(&self.pool)
        .await?;
        Ok(())
    }

    pub async fn heartbeat_resource(&self, resource_id: &str, status: &str) -> Result<()> {
        sqlx::query(
            "UPDATE resources SET status = $1, last_heartbeat = NOW() WHERE resource_id = $2"
        )
        .bind(status)
        .bind(resource_id)
        .execute(&self.pool)
        .await?;
        Ok(())
    }

    pub async fn deregister_resource(&self, resource_id: &str) -> Result<()> {
        sqlx::query(
            "DELETE FROM resources WHERE resource_id = $1"
        )
        .bind(resource_id)
        .execute(&self.pool)
        .await?;
        Ok(())
    }

    pub async fn list_resources(&self) -> Result<Vec<ResourceRecord>> {
        let rows = sqlx::query_as("SELECT * FROM resources ORDER BY last_heartbeat DESC")
            .fetch_all(&self.pool)
            .await?;
        Ok(rows)
    }

    pub async fn list_people(&self, limit: i32, offset: i64) -> Result<Vec<IdentityRecord>> {
        let query = r#"
            SELECT id, uuid, name, metadata, created_at
            FROM identities
            ORDER BY created_at DESC
            LIMIT $1 OFFSET $2
        "#;
        
        let rows = sqlx::query_as(query)
            .bind(limit)
            .bind(offset)
            .fetch_all(&self.pool)
            .await?;
        
        Ok(rows)
    }

    pub async fn search_people(&self, query: &str, limit: i32, offset: i64) -> Result<Vec<IdentityRecord>> {
        let pattern = format!("%{}%", query);
        let sql = r#"
            SELECT id, uuid, name, metadata, created_at
            FROM identities
            WHERE name ILIKE $1
            ORDER BY name ASC
            LIMIT $2 OFFSET $3
        "#;
        
        let rows = sqlx::query_as(sql)
            .bind(pattern)
            .bind(limit)
            .bind(offset)
            .fetch_all(&self.pool)
            .await?;
            
        Ok(rows)
    }

    pub async fn get_people_candidates(&self, limit: i32, offset: i64) -> Result<Vec<CandidateRecord>> {
        let query = r#"
            SELECT id, file_uuid, data, created_at
            FROM pre_chunks
            WHERE processor_type = 'face' AND identity_id IS NULL
            ORDER BY created_at DESC
            LIMIT $1 OFFSET $2
        "#;
        
        let rows = sqlx::query_as(query)
            .bind(limit)
            .bind(offset)
            .fetch_all(&self.pool)
            .await?;
            
        Ok(rows)
    }

    pub async fn list_files(&self, limit: i32, offset: i64) -> Result<Vec<FileRecord>> {
        let query = r#"
            SELECT uuid, file_path, file_name, probe_json, created_at
            FROM videos
            ORDER BY created_at DESC
            LIMIT $1 OFFSET $2
        "#;
        
        let rows = sqlx::query_as(query)
            .bind(limit)
            .bind(offset)
            .fetch_all(&self.pool)
            .await?;
        
        Ok(rows)
    }

    pub async fn confirm_candidate(
        &self,
        pre_chunk_id: i64,
        identity_id: Uuid,
    ) -> Result<()> {
        // 1. Update the pre_chunk to link it to the identity
        sqlx::query(
            "UPDATE pre_chunks SET identity_id = $1 WHERE id = $2"
        )
        .bind(identity_id)
        .bind(pre_chunk_id)
        .execute(&self.pool)
        .await?;

        // 2. Ensure a link exists in file_identities table
        // We need the file_uuid from the pre_chunk
        let file_uuid: Option<Uuid> = sqlx::query_scalar(
            "SELECT file_uuid FROM pre_chunks WHERE id = $1"
        )
        .bind(pre_chunk_id)
        .fetch_optional(&self.pool)
        .await?;

        if let Some(f_uuid) = file_uuid {
            // Check if relationship exists
            let exists: bool = sqlx::query_scalar(
                "SELECT EXISTS(SELECT 1 FROM file_identities WHERE file_uuid = $1 AND identity_id = $2)"
            )
            .bind(f_uuid)
            .bind(identity_id)
            .fetch_one(&self.pool)
            .await?;

            if !exists {
                sqlx::query(
                    "INSERT INTO file_identities (file_uuid, identity_id, status) VALUES ($1, $2, 'detected')"
                )
                .bind(f_uuid)
                .bind(identity_id)
                .execute(&self.pool)
                .await?;
            }
        }

        Ok(())
    }

    pub async fn reject_candidate(&self, pre_chunk_id: i64) -> Result<()> {
        // Just ensure it is NULL (or maybe we mark it as ignored in metadata? For now, just NULL)
        sqlx::query(
            "UPDATE pre_chunks SET identity_id = NULL WHERE id = $1"
        )
        .bind(pre_chunk_id)
        .execute(&self.pool)
        .await?;
        Ok(())
    }

    pub async fn store_chunk(&self, chunk: &Chunk) -> Result<()> {
        let table = schema::table_name("chunks");
        let content_with_rule = serde_json::json!({
            "rule": chunk.rule.as_str(),
            "data": chunk.content
        });

        // 獲取文本內容：優先使用 chunk.text_content，否則從 content 中提取
        let raw_text = chunk.text_content.as_deref().unwrap_or_else(|| {
            // 從 content 中提取文本（支持中文和英文格式）
            chunk
                .content
                .get("data")
                .and_then(|data| data.get("text"))
                .and_then(|v| v.as_str())
                .or_else(|| chunk.content.get("text").and_then(|v| v.as_str()))
                .unwrap_or("")
        });

        // 對中文文本進行分詞
        let tokenized_text = if raw_text.is_empty() {
            None
        } else {
            Some(crate::core::text::tokenizer::tokenize_chinese_text(
                raw_text,
            ))
        };

        sqlx::query(&format!(
            r#"
            INSERT INTO {} (file_id, uuid, chunk_id, chunk_index, chunk_type, start_time, end_time, fps, start_frame, end_frame, text_content, content, metadata, vector_id, frame_count, pre_chunk_ids, parent_chunk_id, child_chunk_ids)
            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12::jsonb, $13::jsonb, $14, $15, $16, $17, $18)
            ON CONFLICT (uuid, chunk_id) DO UPDATE SET
                start_time = EXCLUDED.start_time,
                end_time = EXCLUDED.end_time,
                fps = EXCLUDED.fps,
                start_frame = EXCLUDED.start_frame,
                end_frame = EXCLUDED.end_frame,
                text_content = EXCLUDED.text_content,
                content = EXCLUDED.content,
                metadata = EXCLUDED.metadata,
                vector_id = EXCLUDED.vector_id,
                frame_count = EXCLUDED.frame_count,
                pre_chunk_ids = EXCLUDED.pre_chunk_ids,
                parent_chunk_id = EXCLUDED.parent_chunk_id,
                child_chunk_ids = EXCLUDED.child_chunk_ids,
                updated_at = CURRENT_TIMESTAMP
            "#,
            table
        ))
        .bind(chunk.file_id)
        .bind(&chunk.uuid)
        .bind(&chunk.chunk_id)
        .bind(chunk.chunk_index as i32)
        .bind(chunk.chunk_type.as_str())
        .bind(chunk.start_time().seconds())
        .bind(chunk.end_time().seconds())
        .bind(chunk.fps)
        .bind(chunk.start_frame)
        .bind(chunk.end_frame)
        .bind(&tokenized_text)
        .bind(&content_with_rule)
        .bind(&chunk.metadata)
        .bind(&chunk.vector_id)
        .bind(chunk.frame_count)
        .bind(&chunk.pre_chunk_ids)
        .bind(&chunk.parent_chunk_id)
        .bind(&chunk.child_chunk_ids)
        .execute(&self.pool)
        .await?;

        Ok(())
    }

    pub async fn get_chunks_by_uuid(&self, uuid: &str) -> Result<Vec<Chunk>> {
        let table = schema::table_name("chunks");
        let rows = sqlx::query(&format!(
            "SELECT COALESCE(file_id, 0) as file_id, uuid, chunk_id, chunk_index, chunk_type, COALESCE(fps, 24.0) as fps, COALESCE(start_frame, 0) as start_frame, COALESCE(end_frame, 0) as end_frame, text_content, content, metadata, vector_id, COALESCE(frame_count, 0) as frame_count, pre_chunk_ids, parent_chunk_id::text as parent_chunk_id, child_chunk_ids, visual_stats FROM {} WHERE uuid = $1 ORDER BY chunk_index",
            table
        ))
        .bind(uuid)
        .fetch_all(&self.pool)
        .await?;

        let chunks: Vec<Chunk> = rows
            .into_iter()
            .map(|r| {
                let chunk_type_str: String = r.get(4);
                let chunk_index: i32 = r.get(3);
                let chunk_type = match chunk_type_str.as_str() {
                    "time" => ChunkType::TimeBased,
                    "sentence" => ChunkType::Sentence,
                    "cut" => ChunkType::Cut,
                    "trace" => ChunkType::Trace,
                    "story" => ChunkType::Story,
                    _ => ChunkType::TimeBased,
                };

                let content: serde_json::Value = r.get(9);
                let metadata: Option<serde_json::Value> = r.get(10);

                let pre_chunk_ids: Vec<i32> = r.try_get(13).unwrap_or_default();
                let parent_chunk_id: Option<String> = r.try_get(14).ok().flatten();
                let child_chunk_ids: Vec<String> = r.try_get(15).unwrap_or_default();

                let (rule, content_data) = if content.get("rule").is_some() {
                    let rule_str = content
                        .get("rule")
                        .and_then(|v| v.as_str())
                        .unwrap_or("rule_1");
                    let rule = if rule_str == "rule_2" {
                        ChunkRule::Rule2
                    } else {
                        ChunkRule::Rule1
                    };
                    let data = content.get("data").cloned().unwrap_or(content);
                    (rule, data)
                } else {
                    (ChunkRule::Rule1, content)
                };

                let file_id: i32 = sqlx::Row::get(&r, "file_id");
                let frame_count: i32 = sqlx::Row::get(&r, "frame_count");

                Chunk {
                    file_id,
                    uuid: r.get("uuid"),
                    chunk_id: r.get("chunk_id"),
                    chunk_index: chunk_index as u32,
                    chunk_type,
                    rule,

                    fps: r.get("fps"),
                    start_frame: r.get("start_frame"),
                    end_frame: r.get("end_frame"),
                    text_content: r.get("text_content"),
                    content: content_data,
                    metadata,
                    vector_id: r.get("vector_id"),
                    frame_count,
                    pre_chunk_ids,
                    parent_chunk_id,
                    child_chunk_ids,
                    visual_stats: r.try_get("visual_stats").ok().flatten(),
                }
            })
            .collect();

        Ok(chunks)
    }

    pub async fn get_chunk_by_chunk_id_and_uuid(
        &self,
        chunk_id: &str,
        uuid: &str,
    ) -> Result<Option<Chunk>> {
        let table = schema::table_name("chunks");
        let row = sqlx::query(&format!(
            "SELECT COALESCE(file_id, 0) as file_id, uuid, chunk_id, chunk_index, chunk_type, COALESCE(fps, 24.0) as fps, COALESCE(start_frame, 0) as start_frame, COALESCE(end_frame, 0) as end_frame, text_content, content, metadata, vector_id, COALESCE(frame_count, 0) as frame_count, pre_chunk_ids, parent_chunk_id, child_chunk_ids, visual_stats FROM {} WHERE chunk_id = $1 AND uuid = $2",
            table
        ))
            .bind(chunk_id)
            .bind(uuid)
            .fetch_optional(&self.pool)
            .await?;

        if let Some(r) = row {
            let chunk_type_str: String = r.get(4);
            let chunk_index: i32 = r.get(3);
            let chunk_type = match chunk_type_str.as_str() {
                "time" => ChunkType::TimeBased,
                "sentence" => ChunkType::Sentence,
                "cut" => ChunkType::Cut,
                "trace" => ChunkType::Trace,
                "story" => ChunkType::Story,
                _ => ChunkType::TimeBased,
            };

            let content: serde_json::Value = r.get(9);
            let metadata: Option<serde_json::Value> = r.get(10);

            let pre_chunk_ids: Vec<i32> = r.try_get(13).unwrap_or_default();
            let parent_chunk_id: Option<String> = r.try_get(14).ok().flatten();
            let child_chunk_ids: Vec<String> = r.try_get(15).unwrap_or_default();

            let (rule, content_data) = if content.get("rule").is_some() {
                let rule_str = content
                    .get("rule")
                    .and_then(|v| v.as_str())
                    .unwrap_or("rule_1");
                let rule = if rule_str == "rule_2" {
                    ChunkRule::Rule2
                } else {
                    ChunkRule::Rule1
                };
                let data = content.get("data").cloned().unwrap_or(content);
                (rule, data)
            } else {
                (ChunkRule::Rule1, content)
            };

            let file_id: i32 = sqlx::Row::get(&r, "file_id");
            let frame_count: i32 = sqlx::Row::get(&r, "frame_count");

            Ok(Some(Chunk {
                file_id,
                uuid: r.get("uuid"),
                chunk_id: r.get("chunk_id"),
                chunk_index: chunk_index as u32,
                chunk_type,
                rule,
                fps: r.get("fps"),
                start_frame: r.get("start_frame"),
                end_frame: r.get("end_frame"),
                text_content: r.get("text_content"),
                content: content_data,
                metadata,
                vector_id: r.get("vector_id"),
                frame_count,
                pre_chunk_ids,
                parent_chunk_id,
                child_chunk_ids,
                visual_stats: r.try_get("visual_stats").ok().flatten(),
            }))
        } else {
            Ok(None)
        }
    }

    /// Fetches metadata (including 5W1H Plus) from the parent_chunks table
    pub async fn get_parent_chunk_metadata(
        &self,
        parent_id: i32,
    ) -> Result<Option<serde_json::Value>> {
        let query = "SELECT metadata FROM parent_chunks WHERE id = $1";
        let row: Option<(Option<serde_json::Value>,)> = sqlx::query_as(query)
            .bind(parent_id)
            .fetch_optional(&self.pool)
            .await?;

        Ok(row.map(|r| r.0).flatten())
    }

    /// Fetches extended details from parent_chunks including summary and metadata
    pub async fn get_parent_chunk_detail(
        &self,
        parent_id: i32,
    ) -> Result<Option<(Option<String>, Option<serde_json::Value>)>> {
        let query = "SELECT summary_text, metadata FROM parent_chunks WHERE id = $1";
        eprintln!("[DBG] get_parent_chunk_detail: pid={}", parent_id);
        let row: Option<(Option<String>, Option<serde_json::Value>)> = sqlx::query_as(query)
            .bind(parent_id)
            .fetch_optional(&self.pool)
            .await?;
        eprintln!("[DBG] get_parent_chunk_detail result: {:?}", row);
        Ok(row)
    }

    pub async fn store_pre_chunk(&self, pre_chunk: &PreChunk) -> Result<i64> {
        let table = schema::table_name("pre_chunks");
        let row = sqlx::query(&format!(
            r#"
            INSERT INTO {} (file_id, source_type, source_file, chunk_type, start_time, end_time, start_frame, end_frame, fps, raw_json, text_content, processed, chunk_id)
            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
            ON CONFLICT (file_id, source_type, start_frame, end_frame) DO UPDATE SET
                start_time = EXCLUDED.start_time,
                end_time = EXCLUDED.end_time,
                fps = EXCLUDED.fps,
                raw_json = EXCLUDED.raw_json,
                text_content = EXCLUDED.text_content,
                processed = EXCLUDED.processed,
                chunk_id = EXCLUDED.chunk_id
            RETURNING id
            "#,
            table
        ))
        .bind(pre_chunk.file_id)
        .bind(&pre_chunk.source_type)
        .bind(&pre_chunk.source_file)
        .bind(&pre_chunk.chunk_type)
        .bind(pre_chunk.start_frame as f64 / pre_chunk.fps)
        .bind(pre_chunk.end_frame as f64 / pre_chunk.fps)
        .bind(pre_chunk.start_frame)
        .bind(pre_chunk.end_frame)
        .bind(pre_chunk.fps)
        .bind(&pre_chunk.raw_json)
        .bind(&pre_chunk.text_content)
        .bind(pre_chunk.processed)
        .bind(&pre_chunk.chunk_id)
        .fetch_one(&self.pool)
        .await?;

        let id: i32 = row.get(0);
        Ok(id as i64)
    }

    pub async fn store_frame(&self, frame: &Frame) -> Result<()> {
        let table = schema::table_name("frames");
        sqlx::query(&format!(
            r#"
            INSERT INTO {} (file_id, frame_number, timestamp, fps, yolo_objects, ocr_results, face_results, pose_results, frame_path)
            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
            ON CONFLICT (file_id, frame_number) DO UPDATE SET
                yolo_objects = EXCLUDED.yolo_objects,
                ocr_results = EXCLUDED.ocr_results,
                face_results = EXCLUDED.face_results,
                pose_results = EXCLUDED.pose_results,
                frame_path = EXCLUDED.frame_path
            "#,
            table
        ))
        .bind(frame.file_id)
        .bind(frame.frame_number)
        .bind(frame.timestamp)
        .bind(frame.fps)
        .bind(&frame.yolo_objects)
        .bind(&frame.ocr_results)
        .bind(&frame.face_results)
        .bind(&frame.pose_results)
        .bind(&frame.frame_path)
        .execute(&self.pool)
        .await?;

        Ok(())
    }

    pub async fn get_frames_by_time_range(
        &self,
        file_id: i64,
        start_time: f64,
        end_time: f64,
    ) -> Result<Vec<Frame>> {
        let table = schema::table_name("frames");
        let rows = sqlx::query_as::<_, (
            i32,
            i32,
            i64,
            f64,
            f64,
            Option<serde_json::Value>,
            Option<serde_json::Value>,
            Option<serde_json::Value>,
            Option<serde_json::Value>,
            Option<String>,
            String,
        )>(&format!(
            "SELECT id, file_id, frame_number, timestamp, fps, yolo_objects, ocr_results, face_results, pose_results, frame_path, created_at 
             FROM {} 
             WHERE file_id = $1 AND timestamp >= $2 AND timestamp <= $3 
             ORDER BY frame_number",
            table
        ))
        .bind(file_id)
        .bind(start_time)
        .bind(end_time)
        .fetch_all(&self.pool)
        .await?;

        let frames: Vec<Frame> = rows
            .into_iter()
            .map(|r| Frame {
                id: r.0 as i64,
                file_id: r.1 as i64,
                frame_number: r.2,
                timestamp: r.3,
                fps: r.4,
                yolo_objects: r.5,
                ocr_results: r.6,
                face_results: r.7,
                pose_results: r.8,
                frame_path: r.9,
                created_at: r.10,
            })
            .collect();

        Ok(frames)
    }

    pub async fn get_chunks_by_time_range(
        &self,
        file_id: i64,
        start_time: f64,
        end_time: f64,
    ) -> Result<Vec<Chunk>> {
        let table = schema::table_name("chunks");
        let rows = sqlx::query(&format!(
            "SELECT file_id, uuid, chunk_id, chunk_index, chunk_type, start_time, end_time, fps, start_frame, end_frame, text_content, content, metadata, vector_id, frame_count, pre_chunk_ids, parent_chunk_id::text as parent_chunk_id, child_chunk_ids 
             FROM {} 
             WHERE file_id = $1 AND start_time >= $2 AND end_time <= $3 
             ORDER BY start_time",
            table
        ))
        .bind(file_id)
        .bind(start_time)
        .bind(end_time)
        .fetch_all(&self.pool)
        .await?;

        let chunks: Vec<Chunk> = rows
            .into_iter()
            .map(|r| {
                let chunk_type_str: String = r.get(4);
                let chunk_index: i32 = r.get(3);
                let chunk_type = match chunk_type_str.as_str() {
                    "time" => ChunkType::TimeBased,
                    "sentence" => ChunkType::Sentence,
                    "cut" => ChunkType::Cut,
                    "trace" => ChunkType::Trace,
                    "story" => ChunkType::Story,
                    _ => ChunkType::TimeBased,
                };

                let content: serde_json::Value = r.get(11);
                let metadata: Option<serde_json::Value> = r.get(12);

                let pre_chunk_ids: Vec<i32> = r.try_get(15).unwrap_or_default();
                let parent_chunk_id: Option<String> = r.try_get(16).ok().flatten();
                let child_chunk_ids: Vec<String> = r.try_get(17).unwrap_or_default();

                let (rule, content_data) = if content.get("rule").is_some() {
                    let rule_str = content
                        .get("rule")
                        .and_then(|v| v.as_str())
                        .unwrap_or("rule_1");
                    let rule = if rule_str == "rule_2" {
                        ChunkRule::Rule2
                    } else {
                        ChunkRule::Rule1
                    };
                    let data = content.get("data").cloned().unwrap_or(content);
                    (rule, data)
                } else {
                    (ChunkRule::Rule1, content)
                };

                let file_id: i32 = sqlx::Row::get(&r, "file_id");
                let frame_count: i32 = sqlx::Row::get(&r, "frame_count");

                Chunk {
                    file_id,
                    uuid: r.get("uuid"),
                    chunk_id: r.get("chunk_id"),
                    chunk_index: chunk_index as u32,
                    chunk_type,
                    rule,

                    fps: r.get("fps"),
                    start_frame: r.get("start_frame"),
                    end_frame: r.get("end_frame"),
                    text_content: r.get("text_content"),
                    content: content_data,
                    metadata,
                    vector_id: r.get("vector_id"),
                    frame_count,
                    pre_chunk_ids,
                    parent_chunk_id,
                    child_chunk_ids,
                    visual_stats: r.try_get("visual_stats").ok().flatten(),
                }
            })
            .collect();

        Ok(chunks)
    }

    pub async fn get_chunks_by_ids(&self, chunk_ids: &[String]) -> Result<Vec<Chunk>> {
        if chunk_ids.is_empty() {
            return Ok(vec![]);
        }

        let table = schema::table_name("chunks");
        let rows = sqlx::query(&format!(
            "SELECT file_id, uuid, chunk_id, chunk_index, chunk_type, fps, start_frame, end_frame, text_content, content, metadata, vector_id, frame_count, pre_chunk_ids, parent_chunk_id::text as parent_chunk_id, child_chunk_ids FROM {} WHERE chunk_id = ANY($1) ORDER BY chunk_index",
            table
        ))
        .bind(chunk_ids)
        .fetch_all(&self.pool)
        .await?;

        let chunks: Vec<Chunk> = rows
            .into_iter()
            .map(|r| {
                let chunk_type_str: String = r.get(4);
                let chunk_index: i32 = r.get(3);
                let chunk_type = match chunk_type_str.as_str() {
                    "time" => ChunkType::TimeBased,
                    "sentence" => ChunkType::Sentence,
                    "cut" => ChunkType::Cut,
                    "trace" => ChunkType::Trace,
                    "story" => ChunkType::Story,
                    _ => ChunkType::TimeBased,
                };

                let content: serde_json::Value = r.get(9);
                let metadata: Option<serde_json::Value> = r.get(10);

                let pre_chunk_ids: Vec<i32> = r.try_get(13).unwrap_or_default();
                let parent_chunk_id: Option<String> = r.try_get(14).ok().flatten();
                let child_chunk_ids: Vec<String> = r.try_get(15).unwrap_or_default();

                let (rule, content_data) = if content.get("rule").is_some() {
                    let rule_str = content
                        .get("rule")
                        .and_then(|v| v.as_str())
                        .unwrap_or("rule_1");
                    let rule = if rule_str == "rule_2" {
                        ChunkRule::Rule2
                    } else {
                        ChunkRule::Rule1
                    };
                    let data = content.get("data").cloned().unwrap_or(content);
                    (rule, data)
                } else {
                    (ChunkRule::Rule1, content)
                };

                let file_id: i32 = sqlx::Row::get(&r, "file_id");
                let frame_count: i32 = sqlx::Row::get(&r, "frame_count");

                Chunk {
                    file_id,
                    uuid: r.get("uuid"),
                    chunk_id: r.get("chunk_id"),
                    chunk_index: chunk_index as u32,
                    chunk_type,
                    rule,

                    fps: r.get("fps"),
                    start_frame: r.get("start_frame"),
                    end_frame: r.get("end_frame"),
                    text_content: r.get("text_content"),
                    content: content_data,
                    metadata,
                    vector_id: r.get("vector_id"),
                    frame_count,
                    pre_chunk_ids,
                    parent_chunk_id,
                    child_chunk_ids,
                    visual_stats: r.try_get("visual_stats").ok().flatten(),
                }
            })
            .collect();

        Ok(chunks)
    }

    pub async fn get_file_id_by_uuid(&self, uuid: &str) -> Result<i64> {
        let table = schema::table_name("videos");
        let row = sqlx::query(&format!("SELECT id FROM {} WHERE uuid = $1", table))
            .bind(uuid)
            .fetch_one(&self.pool)
            .await?;

        Ok(row.get(0))
    }

    pub async fn store_vector(&self, chunk_id: &str, vector: &[f32], uuid: &str) -> Result<()> {
        let table = schema::table_name("chunk_vectors");
        let vector_json = serde_json::json!(vector);

        sqlx::query(&format!(
            r#"
            INSERT INTO {} (chunk_id, uuid, chunk_type, embedding)
            VALUES ($1, $2, 'sentence', $3::jsonb)
            ON CONFLICT (chunk_id, uuid) DO UPDATE SET
                embedding = EXCLUDED.embedding
            "#,
            table
        ))
        .bind(chunk_id)
        .bind(uuid)
        .bind(&vector_json)
        .execute(&self.pool)
        .await?;

        tracing::info!("Stored vector for chunk: {}", chunk_id);
        Ok(())
    }

    pub async fn update_vector_id(&self, chunk_id: &str, vector_id: &str) -> Result<()> {
        let table = schema::table_name("chunks");
        sqlx::query(&format!(
            "UPDATE {} SET vector_id = $1 WHERE chunk_id = $2",
            table
        ))
        .bind(vector_id)
        .bind(chunk_id)
        .execute(&self.pool)
        .await?;

        Ok(())
    }

    pub async fn search_vector(
        &self,
        _query_vector: &[f32],
        _limit: usize,
    ) -> Result<Vec<super::SearchResult>> {
        Ok(vec![])
    }

    pub async fn search_text(&self, query: &str, chunk_type: Option<&str>) -> Result<Vec<Chunk>> {
        let table = schema::table_name("chunks");
        let query_pattern = format!("%{}%", query);

        let sql = match chunk_type {
            Some(_) => &format!("SELECT uuid, chunk_id, chunk_index, chunk_type, start_time, end_time, fps, start_frame, end_frame, content, metadata, vector_id, parent_chunk_id, child_chunk_ids FROM {} WHERE content->>'text' ILIKE $1 AND chunk_type = $2 ORDER BY chunk_index", table),
            None => &format!("SELECT uuid, chunk_id, chunk_index, chunk_type, start_time, end_time, fps, start_frame, end_frame, content, metadata, vector_id, parent_chunk_id, child_chunk_ids FROM {} WHERE content->>'text' ILIKE $1 ORDER BY chunk_index", table),
        };

        let chunks = if let Some(ct) = chunk_type {
            sqlx::query_as::<
                _,
                (
                    String,
                    String,
                    i32,
                    String,
                    f64,
                    f64,
                    f64,
                    i64,
                    i64,
                    String,
                    Option<String>,
                    Option<String>,
                    Option<String>,
                    Vec<String>,
                ),
            >(sql)
            .bind(&query_pattern)
            .bind(ct)
            .fetch_all(&self.pool)
            .await?
        } else {
            sqlx::query_as::<
                _,
                (
                    String,
                    String,
                    i32,
                    String,
                    f64,
                    f64,
                    f64,
                    i64,
                    i64,
                    String,
                    Option<String>,
                    Option<String>,
                    Option<String>,
                    Vec<String>,
                ),
            >(sql)
            .bind(&query_pattern)
            .fetch_all(&self.pool)
            .await?
        };

        let results: Vec<Chunk> = chunks
            .into_iter()
            .map(|r| {
                let chunk_type = match r.3.as_str() {
                    "time_based" => ChunkType::TimeBased,
                    "sentence" => ChunkType::Sentence,
                    "cut" => ChunkType::Cut,
                    "trace" => ChunkType::Trace,
                    "story" => ChunkType::Story,
                    _ => ChunkType::TimeBased,
                };

                let content: serde_json::Value =
                    serde_json::from_str(&r.9).unwrap_or(serde_json::json!({}));

                let metadata: Option<serde_json::Value> =
                    r.10.and_then(|m| serde_json::from_str(&m).ok());

                Chunk {
                    file_id: 0,
                    uuid: r.0,
                    chunk_id: r.1,
                    chunk_index: r.2 as u32,
                    chunk_type,
                    rule: ChunkRule::Rule1,
                    fps: r.6,
                    start_frame: r.7,
                    end_frame: r.8,
                    text_content: Some(r.9),
                    content,
                    metadata,
                    vector_id: r.11,
                    frame_count: 0,
                    pre_chunk_ids: vec![],
                    parent_chunk_id: r.12,
                    child_chunk_ids: r.13,
                    visual_stats: None,
                }
            })
            .collect();

        Ok(results)
    }

    pub async fn search_bm25(
        &self,
        query: &str,
        uuid: Option<&str>,
        limit: usize,
    ) -> Result<Vec<Bm25Result>> {
        let table = schema::table_name("chunks");
        let tsquery = self.prepare_tsquery(query).await?;

        let sql = match uuid {
            Some(_) => &format!(
                r#"
                SELECT c.chunk_id, c.uuid, c.chunk_index, c.chunk_type, c.start_frame, c.end_frame, c.fps, c.start_time, c.end_time, 
                       c.text_content, GREATEST(ts_rank_cd(c.search_vector, to_tsquery('english', $1)), ts_rank_cd(pc.summary_tsvector, to_tsquery('english', $1))) as bm25_score,
                       c.visual_stats,
                       pc.metadata->'structured_summary' as scene_summary,
                       c.parent_chunk_id::integer
                FROM {} c
                LEFT JOIN parent_chunks pc ON c.parent_chunk_id = pc.id::varchar
                WHERE (c.search_vector @@ to_tsquery('english', $1) OR pc.summary_tsvector @@ to_tsquery('english', $1)) AND c.uuid = $2
                ORDER BY bm25_score DESC
                LIMIT $3
            "#,
                table
            ),
            None => &format!(
                r#"
                SELECT c.chunk_id, c.uuid, c.chunk_index, c.chunk_type, c.start_frame, c.end_frame, c.fps, c.start_time, c.end_time, 
                       c.text_content, GREATEST(ts_rank_cd(c.search_vector, to_tsquery('english', $1)), ts_rank_cd(pc.summary_tsvector, to_tsquery('english', $1))) as bm25_score,
                       c.visual_stats,
                       pc.metadata->'structured_summary' as scene_summary,
                       c.parent_chunk_id::integer
                FROM {} c
                LEFT JOIN parent_chunks pc ON c.parent_chunk_id = pc.id::varchar
                WHERE (c.search_vector @@ to_tsquery('english', $1) OR pc.summary_tsvector @@ to_tsquery('english', $1))
                ORDER BY bm25_score DESC
                LIMIT $2
            "#,
                table
            ),
        };

        let rows: Vec<(
            String,
            String,
            i32,
            String,
            i64,
            i64,
            f64,
            f64,
            f64,
            Option<String>,
            f32,
            Option<serde_json::Value>,
            Option<serde_json::Value>,
            Option<i32>,
        )> = match uuid {
            Some(_) => {
                sqlx::query_as(sql)
                    .bind(&tsquery)
                    .bind(uuid)
                    .bind(limit as i64)
                    .fetch_all(&self.pool)
                    .await?
            }
            None => {
                sqlx::query_as(sql)
                    .bind(&tsquery)
                    .bind(limit as i64)
                    .fetch_all(&self.pool)
                    .await?
            }
        };

        let results: Vec<Bm25Result> = rows
            .into_iter()
            .map(|r| {
                let scene_summary: Option<SceneSummary> =
                    r.12.as_ref()
                        .and_then(|v| serde_json::from_value(v.clone()).ok());

                Bm25Result {
                    chunk_id: r.0,
                    uuid: r.1,
                    chunk_index: r.2 as u32,
                    chunk_type: r.3,
                    start_frame: r.4,
                    end_frame: r.5,
                    fps: r.6,
                    start_time: r.7,
                    end_time: r.8,
                    text: r.9.unwrap_or_default(),
                    bm25_score: r.10,
                    visual_stats: r.11,
                    scene_summary: r
                        .12
                        .as_ref()
                        .and_then(|v| serde_json::from_value(v.clone()).ok()),
                    parent_chunk_id: r.13,
                }
            })
            .collect();

        Ok(results)
    }

    pub async fn hybrid_search(
        &self,
        query: &str,
        query_vector: &[f32],
        uuid: Option<&str>,
        limit: usize,
        vector_weight: f32,
        bm25_weight: f32,
    ) -> Result<Vec<HybridSearchResult>> {
        tracing::info!(
            "hybrid_search called: query={}, uuid={:?}, limit={}, vector_weight={}, bm25_weight={}",
            query,
            uuid,
            limit,
            vector_weight,
            bm25_weight
        );
        let bm25_results = self.search_bm25(query, uuid, limit * 2).await?;
        tracing::info!("bm25_results count: {}", bm25_results.len());

        let qdrant = QdrantDb::init().await?;
        let vector_results = if let Some(uuid) = uuid {
            qdrant.search_in_uuid(query_vector, uuid, limit * 2).await?
        } else {
            qdrant.search(query_vector, limit * 2).await?
        };
        tracing::info!("vector_results count: {}", vector_results.len());

        let mut combined: std::collections::HashMap<(String, String), HybridSearchResult> =
            std::collections::HashMap::new();

        let max_bm25 = bm25_results
            .first()
            .map(|r| r.bm25_score)
            .unwrap_or(1.0)
            .max(0.001);
        for r in &bm25_results {
            let normalized_score = r.bm25_score / max_bm25;
            let combined_score = (normalized_score * bm25_weight) as f64;
            combined.insert(
                (r.chunk_id.clone(), r.uuid.clone()),
                HybridSearchResult {
                    chunk_id: r.chunk_id.clone(),
                    uuid: r.uuid.clone(),
                    chunk_index: r.chunk_index,
                    chunk_type: r.chunk_type.clone(),
                    start_frame: r.start_frame,
                    end_frame: r.end_frame,
                    fps: r.fps,
                    start_time: r.start_time,
                    end_time: r.end_time,
                    text: r.text.clone(),
                    vector_score: 0.0,
                    bm25_score: normalized_score as f64,
                    combined_score,
                    parent_chunk_id: r.parent_chunk_id,
                    visual_stats: r.visual_stats.clone(),
                },
            );
        }

        let max_vector = vector_results
            .first()
            .map(|r| r.score)
            .unwrap_or(1.0)
            .max(0.001);

        // Build map from (chunk_id, uuid) to Chunk to handle duplicate chunk_ids across videos
        let mut chunk_map: std::collections::HashMap<(String, String), Chunk> =
            std::collections::HashMap::new();
        for search_result in &vector_results {
            if let Ok(Some(chunk)) = self
                .get_chunk_by_chunk_id_and_uuid(&search_result.chunk_id, &search_result.uuid)
                .await
            {
                chunk_map.insert(
                    (search_result.chunk_id.clone(), search_result.uuid.clone()),
                    chunk,
                );
            }
        }

        for r in &vector_results {
            let normalized_score = r.score / max_vector;
            let combined_score = (normalized_score * vector_weight) as f64;
            if let Some(existing) = combined.get_mut(&(r.chunk_id.clone(), r.uuid.clone())) {
                existing.vector_score = normalized_score as f64;
                existing.combined_score += combined_score;
            } else {
                let chunk_data = chunk_map.get(&(r.chunk_id.clone(), r.uuid.clone()));
                let parent_chunk_id = chunk_data
                    .as_ref()
                    .and_then(|c| c.parent_chunk_id.as_ref().and_then(|s| s.parse().ok()));
                combined.insert(
                    (r.chunk_id.clone(), r.uuid.clone()),
                    HybridSearchResult {
                        chunk_id: r.chunk_id.clone(),
                        uuid: r.uuid.clone(),
                        chunk_index: chunk_data.map(|c| c.chunk_index).unwrap_or(0),
                        chunk_type: chunk_data
                            .map(|c| c.chunk_type.as_str().to_string())
                            .unwrap_or_default(),
                        start_frame: chunk_data.map(|c| c.start_frame).unwrap_or(0),
                        end_frame: chunk_data.map(|c| c.end_frame).unwrap_or(0),
                        fps: chunk_data.map(|c| c.fps).unwrap_or(0.0),
                        start_time: chunk_data.map(|c| c.start_time().seconds()).unwrap_or(0.0),
                        end_time: chunk_data.map(|c| c.end_time().seconds()).unwrap_or(0.0),
                        text: chunk_data
                            .and_then(|c| c.text_content.clone())
                            .unwrap_or_default(),
                        vector_score: normalized_score as f64,
                        bm25_score: 0.0,
                        combined_score,
                        parent_chunk_id,
                        visual_stats: chunk_data.and_then(|c| c.visual_stats.clone()),
                    },
                );
            }
        }

        let mut results: Vec<HybridSearchResult> = combined.into_values().collect();
        results.sort_by(|a, b| {
            b.combined_score
                .partial_cmp(&a.combined_score)
                .unwrap_or(std::cmp::Ordering::Equal)
        });
        results.truncate(limit);

        Ok(results)
    }

    pub async fn prepare_tsquery(&self, query: &str) -> Result<String> {
        self.prepare_tsquery_internal_async(query).await
    }

    async fn prepare_tsquery_internal_async(&self, query: &str) -> Result<String> {
        let expander = global_synonym_expander();
        let online_expander = crate::core::text::global_online_expander();

        // 對中文查詢進行特殊處理
        let processed_query = if contains_chinese(query) {
            // 先將簡體中文轉換為繁體中文（假設資料庫儲存繁體中文）
            let normalized = normalize_chinese_query(query);

            // 使用智能同義詞擴展，然後對剩餘部分進行分詞
            let expanded = expander.expand_chinese_query(&normalized);

            // 如果擴展查詢包含 '&'，表示已經進行了同義詞擴展
            if expanded.contains('&') {
                expanded
            } else {
                // 沒有找到同義詞，進行常規分詞
                tokenize_chinese_text(&expanded)
            }
        } else {
            // 對英文查詢：直接使用原始查詢詞，不做同義詞擴展
            // BM25 適合精確匹配，同義詞擴展會導致過多噪音
            // 需要同義詞擴展時應使用 Vector 或 Hybrid 模式
            let words: Vec<&str> = query.split_whitespace().collect();
            let mut cleaned_words: Vec<String> = Vec::new();

            // 英文停用詞
            let stop_words: std::collections::HashSet<&str> = [
                "a", "an", "the", "and", "or", "but", "in", "on", "at", "to", "for", "of", "with",
                "by", "from", "is", "are", "was", "were", "be", "been", "being", "have", "has",
                "had", "do", "does", "did", "will", "would", "could", "should", "may", "might",
                "can", "shall", "it", "its", "this", "that", "these", "those", "i", "you", "he",
                "she", "we", "they", "me", "him", "her", "us", "them", "my", "your", "his", "our",
                "their", "what", "which", "who", "whom", "whose", "where", "when", "why", "how",
                "not", "no", "so", "if", "then", "than", "too", "very", "just", "about", "up",
                "out", "into", "over", "after", "before", "between", "under", "again", "further",
                "once", "here", "there", "all", "each", "few", "more", "most", "other", "some",
                "such", "only", "own", "same", "also", "back", "down", "off", "above", "below",
                "during", "through", "while", "until", "whether",
            ]
            .iter()
            .cloned()
            .collect();

            for word in words {
                let cleaned = word
                    .chars()
                    .filter(|c| c.is_alphanumeric())
                    .collect::<String>()
                    .to_lowercase();

                if !cleaned.is_empty() && !stop_words.contains(cleaned.as_str()) {
                    cleaned_words.push(format!("{}:*", cleaned));
                }
            }

            if cleaned_words.is_empty() {
                return Ok("__no_match__:*".to_string());
            }

            // 使用 & 連接所有詞 (AND 邏輯)，加上前綴匹配
            return Ok(cleaned_words.join(" & "));
        };

        // 解析查詢字符串，處理同義詞組
        let groups = Self::parse_query_groups(&processed_query);

        let mut tsquery_groups = Vec::new();

        for group in groups {
            if group.is_empty() {
                continue;
            }

            // 檢查是否為同義詞組（格式: (詞語1 | 詞語2 | ...)）
            let terms: Vec<&str> = if group.starts_with('(') && group.ends_with(')') {
                // 提取括號內的詞語
                let inner = &group[1..group.len() - 1];
                inner.split('|').map(|s| s.trim()).collect()
            } else {
                // 單個詞語
                vec![group.as_str()]
            };

            // 為每個詞語生成 tsquery 片段
            let mut term_tsqueries = Vec::new();

            for term in terms {
                // 將詞語按空白字符分割（處理像 "電 腦" 這樣的已分詞詞語）
                let parts: Vec<&str> = term.split_whitespace().collect();

                // 清理每個部分並加上前綴搜索符號
                let cleaned_parts: Vec<String> = parts
                    .iter()
                    .map(|part| {
                        // 保留字母数字字符和Unicode字母字符（包括中文）
                        let cleaned = part
                            .chars()
                            .filter(|c| c.is_alphanumeric() || c.is_alphabetic())
                            .collect::<String>();
                        if cleaned.is_empty() {
                            None
                        } else {
                            Some(format!("{}:*", cleaned.to_lowercase()))
                        }
                    })
                    .flatten()
                    .collect();

                if cleaned_parts.is_empty() {
                    continue; // 跳過無效部分
                }

                // 如果只有一個部分，直接使用；多個部分用 AND 連接
                let term_tsquery = if cleaned_parts.len() == 1 {
                    cleaned_parts[0].clone()
                } else {
                    cleaned_parts.join(" & ")
                };

                term_tsqueries.push(term_tsquery);
            }

            if term_tsqueries.is_empty() {
                continue; // 跳過無效詞語組
            }

            // 如果只有一個詞語 tsquery，不需括號；多個詞語用括號和 OR 連接
            let tsquery_group = if term_tsqueries.len() == 1 {
                term_tsqueries[0].clone()
            } else {
                format!("({})", term_tsqueries.join(" | "))
            };
            tsquery_groups.push(tsquery_group);
        }

        // 如果没有可搜索的术语，返回一个不会匹配任何内容的安全查询
        // 而不是报错，这样BM25搜索将返回空结果，但不会导致500错误
        if tsquery_groups.is_empty() {
            return Ok("__no_match__:*".to_string());
        }

        Ok(tsquery_groups.join(" & "))
    }

    /// 解析查詢字符串，識別同義詞組（用括號包圍的部分）
    fn parse_query_groups(query: &str) -> Vec<String> {
        let mut groups = Vec::new();
        let mut current_group = String::new();
        let mut paren_depth = 0;

        for ch in query.chars() {
            match ch {
                '(' => {
                    if paren_depth > 0 {
                        current_group.push(ch);
                    }
                    paren_depth += 1;
                    current_group.push(ch);
                }
                ')' => {
                    paren_depth -= 1;
                    current_group.push(ch);
                    if paren_depth == 0 {
                        groups.push(current_group.trim().to_string());
                        current_group.clear();
                    }
                }
                '&' if paren_depth == 0 => {
                    // 在同義詞組外遇到 &，分隔符
                    if !current_group.trim().is_empty() {
                        groups.push(current_group.trim().to_string());
                        current_group.clear();
                    }
                }
                _ if paren_depth == 0 && ch.is_whitespace() => {
                    // 在同義詞組外遇到空白，分隔符
                    if !current_group.trim().is_empty() {
                        groups.push(current_group.trim().to_string());
                        current_group.clear();
                    }
                }
                _ => {
                    current_group.push(ch);
                }
            }
        }

        // 處理最後一個組
        if !current_group.trim().is_empty() {
            groups.push(current_group.trim().to_string());
        }

        groups
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SceneSummary {
    #[serde(rename = "summary_5lines")]
    pub summary: String,
    pub who: String,
    pub what: String,
    pub r#where: String,
    pub when: Option<String>,
    pub why: String,
    pub how: String,
    pub tone: Vec<String>,
    pub characters: Vec<String>,
    pub key_events: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Bm25Result {
    pub chunk_id: String,
    pub uuid: String,
    pub chunk_index: u32,
    pub chunk_type: String,
    pub start_frame: i64,
    pub end_frame: i64,
    pub fps: f64,
    pub start_time: f64,
    pub end_time: f64,
    pub text: String,
    pub bm25_score: f32,
    pub parent_chunk_id: Option<i32>,
    pub visual_stats: Option<serde_json::Value>,
    pub scene_summary: Option<SceneSummary>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HybridSearchResult {
    pub uuid: String,
    pub chunk_id: String,
    pub chunk_index: u32,
    pub chunk_type: String,
    pub start_frame: i64,
    pub end_frame: i64,
    pub fps: f64,
    pub start_time: f64,
    pub end_time: f64,
    pub text: String,
    pub vector_score: f64,
    pub bm25_score: f64,
    pub combined_score: f64,
    pub parent_chunk_id: Option<i32>,
    pub visual_stats: Option<serde_json::Value>,
}

impl PostgresDb {
    /// Search person_identities for n8n Who Search
    pub async fn search_person_candidates(
        &self,
        query: &str,
        uuid: &Option<String>,
        limit: i32,
    ) -> Result<Vec<serde_json::Value>> {
        let person_identities = schema::table_name("person_identities");
        let search_query = format!("%{}%", query);

        let sql = match uuid {
            Some(_) => &format!(
                "SELECT person_id, name, appearance_count, video_uuid, created_at 
                 FROM {} WHERE name ILIKE $1 AND video_uuid = $2
                 ORDER BY appearance_count DESC LIMIT $3",
                person_identities
            ),
            None => &format!(
                "SELECT person_id, name, appearance_count, video_uuid, created_at 
                 FROM {} WHERE name ILIKE $1
                 ORDER BY appearance_count DESC LIMIT $2",
                person_identities
            ),
        };

        let rows: Vec<(
            String,
            String,
            i32,
            String,
            Option<chrono::DateTime<chrono::Utc>>,
        )> = match uuid {
            Some(_) => {
                sqlx::query_as(sql)
                    .bind(&search_query)
                    .bind(uuid.as_ref().unwrap())
                    .bind(limit as i64)
                    .fetch_all(&self.pool)
                    .await?
            }
            None => {
                sqlx::query_as(sql)
                    .bind(&search_query)
                    .bind(limit as i64)
                    .fetch_all(&self.pool)
                    .await?
            }
        };

        let results: Vec<serde_json::Value> = rows
            .into_iter()
            .map(|r| {
                serde_json::json!({
                    "person_id": r.0,
                    "name": r.1,
                    "appearance_count": r.2,
                    "video_uuid": r.3,
                    "created_at": r.4.map(|t| t.to_string())
                })
            })
            .collect();

        Ok(results)
    }

    pub async fn get_pending_jobs(&self, limit: i32) -> Result<Vec<MonitorJob>> {
        let monitor_jobs = schema::table_name("monitor_jobs");
        let rows = sqlx::query(&format!(
            r#"
            SELECT id, uuid, video_path, status, current_processor, progress_total, progress_current,
                   error_count, last_error, started_at, updated_at, created_at
            FROM {}
            WHERE status IN ('pending', 'running')
            ORDER BY created_at ASC
            LIMIT $1
            "#,
            monitor_jobs
        ))
        .bind(limit)
        .fetch_all(&self.pool)
        .await?;

        let jobs: Vec<MonitorJob> = rows
            .into_iter()
            .map(|r| {
                let status_str: String = r.get(3);
                let status =
                    MonitorJobStatus::from_db_str(&status_str).unwrap_or(MonitorJobStatus::Pending);
                MonitorJob {
                    id: r.get(0),
                    uuid: r.get(1),
                    video_path: r.get(2),
                    status,
                    current_processor: r.get(4),
                    progress_total: r.get(5),
                    progress_current: r.get(6),
                    error_count: r.get(7),
                    last_error: r.get(8),
                    started_at: r.get(9),
                    updated_at: r.get(10),
                    created_at: r.get(11),
                }
            })
            .collect();

        Ok(jobs)
    }

    pub async fn get_running_jobs_with_all_processors_done(
        &self,
        limit: i32,
    ) -> Result<Vec<MonitorJob>> {
        let monitor_jobs = schema::table_name("monitor_jobs");
        let processor_results = schema::table_name("processor_results");
        let rows = sqlx::query(&format!(
            r#"
            SELECT id, uuid, video_path, status, current_processor, progress_total, progress_current,
                   error_count, last_error, started_at, updated_at, created_at
            FROM {}
            WHERE status = 'running'
              AND NOT EXISTS (
                  SELECT 1 FROM {} pr 
                  WHERE pr.job_id = monitor_jobs.id 
                  AND pr.status IN ('pending', 'running')
              )
            ORDER BY updated_at ASC
            LIMIT $1
            FOR UPDATE SKIP LOCKED
            "#,
            monitor_jobs, processor_results
        ))
        .bind(limit)
        .fetch_all(&self.pool)
        .await?;

        let jobs: Vec<MonitorJob> = rows
            .into_iter()
            .map(|r| {
                let status_str: String = r.get(3);
                let status =
                    MonitorJobStatus::from_db_str(&status_str).unwrap_or(MonitorJobStatus::Pending);
                MonitorJob {
                    id: r.get(0),
                    uuid: r.get(1),
                    video_path: r.get(2),
                    status,
                    current_processor: r.get(4),
                    progress_total: r.get(5),
                    progress_current: r.get(6),
                    error_count: r.get(7),
                    last_error: r.get(8),
                    started_at: r.get(9),
                    updated_at: r.get(10),
                    created_at: r.get(11),
                }
            })
            .collect();

        Ok(jobs)
    }

    pub async fn update_job_processors_arrays(
        &self,
        job_id: i32,
        completed_processors: Vec<String>,
        failed_processors: Vec<String>,
    ) -> Result<()> {
        let table = schema::table_name("monitor_jobs");
        sqlx::query(&format!(
            "UPDATE {} 
             SET completed_processors = $1, 
                 failed_processors = $2, 
                 updated_at = CURRENT_TIMESTAMP 
             WHERE id = $3",
            table
        ))
        .bind(completed_processors)
        .bind(failed_processors)
        .bind(job_id)
        .execute(&self.pool)
        .await?;
        Ok(())
    }

    pub async fn update_job_status(&self, job_id: i32, status: MonitorJobStatus) -> Result<()> {
        let table = schema::table_name("monitor_jobs");
        sqlx::query(&format!(
            "UPDATE {} SET status = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2",
            table
        ))
        .bind(status.as_str())
        .bind(job_id)
        .execute(&self.pool)
        .await?;
        Ok(())
    }

    pub async fn update_job_progress(
        &self,
        job_id: i32,
        current_processor: Option<&str>,
        progress_current: i32,
    ) -> Result<()> {
        let table = schema::table_name("monitor_jobs");
        sqlx::query(&format!(
            r#"
            UPDATE {}
            SET current_processor = $1, progress_current = $2, updated_at = CURRENT_TIMESTAMP
            WHERE id = $3
            "#,
            table
        ))
        .bind(current_processor)
        .bind(progress_current)
        .bind(job_id)
        .execute(&self.pool)
        .await?;
        Ok(())
    }

    pub async fn create_processor_result(
        &self,
        job_id: i32,
        processor_type: ProcessorType,
    ) -> Result<i32> {
        let table = schema::table_name("processor_results");
        let row = sqlx::query(&format!(
            r#"
            INSERT INTO {} (job_id, processor, status)
            VALUES ($1, $2, 'pending')
            ON CONFLICT (job_id, processor) DO UPDATE SET job_id = EXCLUDED.job_id
            RETURNING id
            "#,
            table
        ))
        .bind(job_id)
        .bind(processor_type.as_str())
        .fetch_one(&self.pool)
        .await?;

        let id: i32 = row.get(0);
        Ok(id)
    }

    pub async fn update_processor_result(
        &self,
        id: i32,
        status: ProcessorJobStatus,
        error_message: Option<&str>,
        output_data: Option<&serde_json::Value>,
    ) -> Result<()> {
        let table = schema::table_name("processor_results");
        sqlx::query(&format!(
            r#"
            UPDATE {}
            SET status = $1,
                error_message = $2,
                output_data = $3,
                started_at = CASE WHEN $1 = 'running' AND started_at IS NULL THEN CURRENT_TIMESTAMP ELSE started_at END,
                completed_at = CASE WHEN $1 IN ('completed', 'failed', 'skipped') THEN CURRENT_TIMESTAMP ELSE completed_at END,
                updated_at = CURRENT_TIMESTAMP
            WHERE id = $4
            "#,
            table
        ))
        .bind(status.as_str())
        .bind(error_message)
        .bind(output_data)
        .bind(id)
        .execute(&self.pool)
        .await?;
        Ok(())
    }

    pub async fn get_processor_results_by_job(&self, job_id: i32) -> Result<Vec<ProcessorResult>> {
        let table = schema::table_name("processor_results");
        let rows = sqlx::query(&format!(
            r#"
            SELECT id, job_id, processor, status, output_path, started_at, completed_at,
                   error_message, progress_total, progress_current, last_checkpoint,
                     created_at, updated_at, duration_secs
            FROM {}
            WHERE job_id = $1
            ORDER BY created_at ASC
            "#,
            table
        ))
        .bind(job_id)
        .fetch_all(&self.pool)
        .await?;

        let results: Vec<ProcessorResult> = rows
            .into_iter()
            .map(|r| {
                let status_str: String = r.get(3);
                let processor_type_str: String = r.get(2);
                let started_at: Option<chrono::DateTime<chrono::Utc>> = r.get(5);
                let completed_at: Option<chrono::DateTime<chrono::Utc>> = r.get(6);
                let created_at: chrono::DateTime<chrono::Utc> = r.get(11);
                let updated_at: Option<chrono::DateTime<chrono::Utc>> = r.get(12);
                ProcessorResult {
                    id: r.get(0),
                    job_id: r.get(1),
                    processor_type: ProcessorType::from_db_str(&processor_type_str)
                        .unwrap_or(ProcessorType::Asr),
                    status: ProcessorJobStatus::from_db_str(&status_str)
                        .unwrap_or(ProcessorJobStatus::Pending),
                    started_at: started_at.map(|t| t.to_string()),
                    completed_at: completed_at.map(|t| t.to_string()),
                    duration_secs: r.get(13),
                    error_message: r.get(7),
                    output_data: None,
                    retry_count: 0,
                    created_at: created_at.to_string(),
                    updated_at: updated_at.map(|t| t.to_string()).unwrap_or_default(),
                }
            })
            .collect();

        Ok(results)
    }

    pub async fn get_video_status(&self, uuid: &str) -> Result<Option<VideoStatus>> {
        let table = schema::table_name("videos");
        let result: Option<String> =
            sqlx::query_scalar(&format!("SELECT status FROM {} WHERE uuid = $1", table))
                .bind(uuid)
                .fetch_optional(&self.pool)
                .await?;

        Ok(result.and_then(|s| VideoStatus::from_db_str(&s)))
    }

    pub async fn update_video_status(&self, uuid: &str, status: VideoStatus) -> Result<()> {
        let table = schema::table_name("videos");
        sqlx::query(&format!(
            "UPDATE {} SET status = $1, updated_at = CURRENT_TIMESTAMP WHERE uuid = $2",
            table
        ))
        .bind(status.as_str())
        .bind(uuid)
        .execute(&self.pool)
        .await?;
        Ok(())
    }

    pub async fn get_running_job_count(&self) -> Result<i64> {
        let table = schema::table_name("monitor_jobs");
        let count: i64 = sqlx::query_scalar(&format!(
            "SELECT COUNT(*) FROM {} WHERE status = 'running'",
            table
        ))
        .fetch_one(&self.pool)
        .await?;
        Ok(count)
    }

    // ==========================================
    // 身份綁定系統 (Identity Binding V5)
    // ==========================================

    /// 獲取或創建 Identity
    pub async fn get_or_create_identity(
        &self,
        name: &str,
    ) -> Result<crate::core::person_identity::Identity> {
        let identity = sqlx::query_as::<_, crate::core::person_identity::Identity>(
            r#"INSERT INTO identities (name) VALUES ($1) ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name RETURNING id, name, embedding::text, metadata, created_at"#,
        )
        .bind(name)
        .fetch_one(&self.pool)
        .await?;
        Ok(identity)
    }

    /// 綁定身份
    pub async fn bind_identity(
        &self,
        identity_id: i64,
        binding_type: &str,
        binding_value: &str,
        source: &str,
        confidence: f64,
    ) -> Result<()> {
        sqlx::query(
            r#"INSERT INTO identity_bindings (identity_id, identity_type, identity_value, metadata, confidence) VALUES ($1, $2, $3, jsonb_build_object('source', $4), $5) ON CONFLICT (identity_id, identity_type, identity_value) DO UPDATE SET confidence = EXCLUDED.confidence"#,
        )
        .bind(identity_id)
        .bind(binding_type)
        .bind(binding_value)
        .bind(source)
        .bind(confidence)
        .execute(&self.pool)
        .await?;
        Ok(())
    }

    /// 解綁身份
    pub async fn unbind_identity(&self, identity_type: &str, identity_value: &str) -> Result<()> {
        sqlx::query(
            "DELETE FROM identity_bindings WHERE identity_type = $1 AND identity_value = $2",
        )
        .bind(identity_type)
        .bind(identity_value)
        .execute(&self.pool)
        .await?;
        Ok(())
    }

    /// 查詢機器 ID 對應的 Identity
    pub async fn get_identity_by_binding(
        &self,
        binding_type: &str,
        binding_value: &str,
    ) -> Result<Option<crate::core::person_identity::Identity>> {
        let identity = sqlx::query_as::<_, crate::core::person_identity::Identity>(
            "SELECT i.id, i.name, i.embedding::text, i.metadata, i.created_at FROM identities i JOIN identity_bindings b ON i.id = b.identity_id WHERE b.identity_type = $1 AND b.identity_value = $2",
        )
        .bind(binding_type)
        .bind(binding_value)
        .fetch_optional(&self.pool)
        .await?;
        Ok(identity)
    }

    /// 列出所有 Identities
    pub async fn list_identities(
        &self,
        search: &str,
        limit: i32,
        offset: i32,
    ) -> Result<Vec<crate::core::person_identity::Identity>> {
        let query = if !search.is_empty() {
            sqlx::query_as::<_, crate::core::person_identity::Identity>(
                "SELECT id, name, embedding::text, metadata, created_at FROM identities WHERE name ILIKE $1 ORDER BY id LIMIT $2 OFFSET $3",
            )
            .bind(format!("%{}%", search))
        } else {
            sqlx::query_as::<_, crate::core::person_identity::Identity>(
                "SELECT id, name, embedding::text, metadata, created_at FROM identities ORDER BY id LIMIT $1 OFFSET $2",
            )
        };
        let identities = query.bind(limit).bind(offset).fetch_all(&self.pool).await?;
        Ok(identities)
    }

    /// 根據 ID 獲取 Identity
    pub async fn get_identity_by_id(
        &self,
        id: i64,
    ) -> Result<Option<crate::core::person_identity::Identity>> {
        let identity = sqlx::query_as::<_, crate::core::person_identity::Identity>(
            "SELECT id, name, embedding::text, metadata, created_at FROM identities WHERE id = $1",
        )
        .bind(id)
        .fetch_optional(&self.pool)
        .await?;
        Ok(identity)
    }

    // ==========================================
    // 信號發現與管理 (Signal Discovery)
    // ==========================================

    // ==========================================
    // 5W1H Smart Search
    // ==========================================

    /// 搜尋 Parent Chunks (Semantic Vector Search)
    pub async fn search_parent_chunks_semantic(
        &self,
        uuid: &str,
        query_vector: &[f32],
        limit: usize,
    ) -> Result<Vec<SemanticSearchResult>> {
        // Convert Vec<f32> to JSON string for vector cast
        let vector_json = serde_json::to_string(query_vector)
            .map_err(|e| anyhow::anyhow!("Vector serialize error: {}", e))?;

        let results = sqlx::query_as::<_, SemanticSearchResult>(
            r#"
            SELECT 
                id, scene_order, start_time, end_time, 
                summary_text as summary,
                metadata,
                (1 - (summary_vector <=> $1::vector)) as similarity
            FROM parent_chunks
            WHERE uuid = $2
            ORDER BY summary_vector <=> $1::vector
            LIMIT $3
            "#,
        )
        .bind(&vector_json)
        .bind(uuid)
        .bind(limit as i64)
        .fetch_all(&self.pool)
        .await?;

        Ok(results)
    }

    /// Get children for a list of parent IDs
    pub async fn get_children_for_parents(
        &self,
        parent_ids: &[i32],
        limit_per_parent: usize,
    ) -> Result<Vec<ChildChunkResult>> {
        if parent_ids.is_empty() {
            return Ok(vec![]);
        }

        // Use subquery to fetch top N children per parent using row_number()
        let results = sqlx::query_as::<_, ChildChunkResult>(
            r#"
            SELECT id, parent_id, start_frame, end_frame, fps, start_time, end_time, raw_text, speaker_ids
            FROM (
                SELECT *, 
                       ROW_NUMBER() OVER (PARTITION BY parent_id ORDER BY start_time) as rn
                FROM child_chunks 
                WHERE parent_id = ANY($1)
            ) sub
            WHERE rn <= $2
            ORDER BY parent_id, start_time
            "#,
        )
        .bind(parent_ids)
        .bind(limit_per_parent as i64)
        .fetch_all(&self.pool)
        .await?;

        Ok(results)
    }

    /// List all persons (Legacy / Global)
    pub async fn list_all_persons(&self) -> Result<Vec<crate::api::who::WhoIdentity>> {
        let rows = sqlx::query_as::<_, (i32, String, String, Option<serde_json::Value>)>(
            "SELECT id, uuid, name, metadata FROM video_identities ORDER BY created_at DESC",
        )
        .fetch_all(&self.pool)
        .await?;

        let mut results = Vec::new();
        for (id, uuid, name, meta) in rows {
            if let Some(identity) = self.build_who_identity(&uuid, id, name, meta).await? {
                results.push(identity);
            }
        }
        Ok(results)
    }

    /// Get Who info by Chunk ID (Parent or Child)
    pub async fn get_who_info_by_chunk(
        &self,
        uuid: &str,
        chunk_id: &str,
    ) -> Result<serde_json::Value> {
        // 1. Find Chunk Time Range
        // Check Parent
        let mut res = sqlx::query_as::<_, (f64, f64)>(
            "SELECT start_time, end_time FROM parent_chunks WHERE uuid = $1 AND id::text = $2",
        )
        .bind(uuid)
        .bind(chunk_id)
        .fetch_optional(&self.pool)
        .await?;

        // If not found, Check Child
        if res.is_none() {
            res = sqlx::query_as::<_, (f64, f64)>(
                "SELECT start_time, end_time FROM child_chunks WHERE uuid = $1 AND id::text = $2",
            )
            .bind(uuid)
            .bind(chunk_id)
            .fetch_optional(&self.pool)
            .await?;
        }

        let (start, end) = match res {
            Some(t) => t,
            None => return Ok(serde_json::json!({ "error": "Chunk not found" })),
        };

        // 2. Aggregate Face & Speaker IDs within Time Range
        let faces: Vec<String> = sqlx::query_scalar(
            "SELECT DISTINCT unnest(face_ids) FROM child_chunks WHERE uuid = $1 AND start_time >= $2 AND end_time <= $3 AND face_ids IS NOT NULL AND face_ids <> '{}'"
        )
        .bind(uuid)
        .bind(start)
        .bind(end)
        .fetch_all(&self.pool)
        .await?;

        let speakers: Vec<String> = sqlx::query_scalar(
            "SELECT DISTINCT unnest(speaker_ids) FROM child_chunks WHERE uuid = $1 AND start_time >= $2 AND end_time <= $3 AND speaker_ids IS NOT NULL AND speaker_ids <> '{}'"
        )
        .bind(uuid)
        .bind(start)
        .bind(end)
        .fetch_all(&self.pool)
        .await?;

        // 3. Resolve to Person IDs
        let mut persons: std::collections::HashMap<String, String> =
            std::collections::HashMap::new(); // Map ID -> Name

        // Resolve Faces
        for fid in &faces {
            if let Some(identity) = self.find_person_by_binding(uuid, "face", fid).await? {
                persons.insert(format!("face_{}", fid), identity.name);
            }
        }

        // Resolve Speakers
        for sid in &speakers {
            if let Some(identity) = self.find_person_by_binding(uuid, "speaker", sid).await? {
                persons.insert(format!("speaker_{}", sid), identity.name);
            }
        }

        Ok(serde_json::json!({
            "uuid": uuid,
            "chunk_id": chunk_id,
            "time_range": { "start": start, "end": end },
            "raw_ids": {
                "face_ids": faces,
                "speaker_ids": speakers
            },
            "resolved_persons": persons
        }))
    }

    /// Create or Update Person (Define Identity - Video Scoped)
    pub async fn create_or_update_person(
        &self,
        uuid: &str,
        identity_id: Option<i32>, // If None, create new
        name: String,
        face_ids: Vec<String>,
        speaker_ids: Vec<String>,
    ) -> Result<crate::api::who::WhoIdentity> {
        let final_id = if let Some(id) = identity_id {
            // Update Name (Scoped to UUID check implicit by ID, but let's be safe)
            sqlx::query("UPDATE video_identities SET name = $1 WHERE id = $2 AND uuid = $3")
                .bind(&name)
                .bind(id)
                .bind(uuid)
                .execute(&self.pool)
                .await?;
            id
        } else {
            // Create New
            let id = sqlx::query(
                "INSERT INTO video_identities (uuid, name) VALUES ($1, $2) RETURNING id",
            )
            .bind(uuid)
            .bind(&name)
            .fetch_one(&self.pool)
            .await?;
            id.get(0)
        };

        // Clear old bindings for THIS identity
        sqlx::query("DELETE FROM identity_bindings WHERE identity_id = $1")
            .bind(final_id)
            .execute(&self.pool)
            .await?;

        // Bind Faces
        for fid in face_ids {
            if !fid.is_empty() {
                sqlx::query("INSERT INTO identity_bindings (identity_id, uuid, binding_type, binding_value) VALUES ($1, $2, 'face', $3) ON CONFLICT (uuid, binding_type, binding_value) DO UPDATE SET identity_id = EXCLUDED.identity_id")
                    .bind(final_id)
                    .bind(uuid)
                    .bind(&fid)
                    .execute(&self.pool)
                    .await?;
            }
        }

        // Bind Speakers
        for sid in speaker_ids {
            if !sid.is_empty() {
                sqlx::query("INSERT INTO identity_bindings (identity_id, uuid, binding_type, binding_value) VALUES ($1, $2, 'speaker', $3) ON CONFLICT (uuid, binding_type, binding_value) DO UPDATE SET identity_id = EXCLUDED.identity_id")
                    .bind(final_id)
                    .bind(uuid)
                    .bind(&sid)
                    .execute(&self.pool)
                    .await?;
            }
        }

        self.get_person_by_id(uuid, final_id)
            .await?
            .ok_or_else(|| anyhow::anyhow!("Failed to retrieve created person"))
    }

    /// Get Person by ID (Video Scoped)
    pub async fn get_person_by_id(
        &self,
        uuid: &str,
        id: i32,
    ) -> Result<Option<crate::api::who::WhoIdentity>> {
        let res = sqlx::query_as::<_, (i32, String, Option<serde_json::Value>)>(
            "SELECT id, name, metadata FROM video_identities WHERE id = $1 AND uuid = $2",
        )
        .bind(id)
        .bind(uuid)
        .fetch_optional(&self.pool)
        .await?;

        match res {
            Some((id, name, meta)) => self.build_who_identity(uuid, id, name, meta).await,
            None => Ok(None),
        }
    }

    pub async fn list_unbound_signals(
        &self,
        uuid: &str,
        binding_type: &str,
    ) -> Result<Vec<String>> {
        let column = if binding_type == "face" {
            "face_ids"
        } else {
            "speaker_ids"
        };

        let query = format!(
            r#"
            SELECT DISTINCT unnest({})::varchar as signal_val 
            FROM chunks
            WHERE uuid = $1 AND {} IS NOT NULL AND {} <> '{{}}'
            EXCEPT
            SELECT identity_value FROM identity_bindings WHERE identity_type = $2
            ORDER BY signal_val
            "#,
            column, column, column
        );

        let rows: Vec<(String,)> = sqlx::query_as(&query)
            .bind(uuid)
            .bind(binding_type)
            .fetch_all(&self.pool)
            .await?;

        Ok(rows.into_iter().map(|r| r.0).collect())
    }

    /// 獲取包含特定信號的所有 Chunk (用於標註驗證)
    pub async fn get_chunks_by_signal(
        &self,
        uuid: &str,
        binding_type: &str,
        signal_value: &str,
    ) -> Result<Vec<serde_json::Value>> {
        let column = if binding_type == "face" {
            "face_ids"
        } else {
            "speaker_ids"
        };
        let query = format!(
            "SELECT id, start_frame, end_frame, content FROM chunks WHERE uuid = $1 AND $2::text = ANY({}::text[]) ORDER BY start_frame",
            column
        );

        let rows = sqlx::query(&query)
            .bind(uuid)
            .bind(signal_value)
            .fetch_all(&self.pool)
            .await?;

        let chunks: Vec<serde_json::Value> = rows
            .into_iter()
            .map(|r| {
                let content: Option<serde_json::Value> = r.get(3);
                let mut result = serde_json::json!({
                    "id": r.get::<i32, _>(0),
                    "start_frame": r.get::<i64, _>(1),
                    "end_frame": r.get::<i64, _>(2),
                });
                if let Some(c) = content {
                    result["content"] = c;
                }
                result
            })
            .collect();

        Ok(chunks)
    }

    // ==========================================
    // Who API Helpers
    // ==========================================

    /// Helper to find person by binding
    pub async fn find_person_by_binding(
        &self,
        uuid: &str,
        bind_type: &str,
        bind_value: &str,
    ) -> Result<Option<crate::api::who::WhoIdentity>> {
        let res = sqlx::query_as::<_, (i32, String, Option<serde_json::Value>)>(
            "SELECT vi.id, vi.name, vi.metadata 
             FROM video_identities vi 
             JOIN identity_bindings ib ON vi.id = ib.identity_id 
             WHERE vi.uuid = $1 AND ib.binding_type = $2 AND ib.binding_value = $3",
        )
        .bind(uuid)
        .bind(bind_type)
        .bind(bind_value)
        .fetch_optional(&self.pool)
        .await?;

        match res {
            Some((id, name, meta)) => self.build_who_identity(uuid, id, name, meta).await,
            None => Ok(None),
        }
    }

    /// Helper to build full WhoIdentity with all linked IDs
    pub async fn build_who_identity(
        &self,
        uuid: &str,
        id: i32,
        name: String,
        meta: Option<serde_json::Value>,
    ) -> Result<Option<crate::api::who::WhoIdentity>> {
        // Fetch all Face IDs
        let face_rows = sqlx::query_as::<_, (String,)>(
            "SELECT binding_value FROM identity_bindings WHERE identity_id = $1 AND binding_type = 'face'"
        )
        .bind(id)
        .fetch_all(&self.pool)
        .await?;
        let face_ids: Vec<String> = face_rows.into_iter().map(|r| r.0).collect();

        // Fetch all Speaker IDs
        let speaker_rows = sqlx::query_as::<_, (String,)>(
            "SELECT binding_value FROM identity_bindings WHERE identity_id = $1 AND binding_type = 'speaker'"
        )
        .bind(id)
        .fetch_all(&self.pool)
        .await?;
        let speaker_ids: Vec<String> = speaker_rows.into_iter().map(|r| r.0).collect();

        Ok(Some(crate::api::who::WhoIdentity {
            identity_id: id,
            uuid: uuid.to_string(),
            name,
            tags: meta
                .and_then(|m| m.get("tags").cloned())
                .and_then(|v| serde_json::from_value(v).ok()),
            face_ids,
            speaker_ids,
        }))
    }
}

#[async_trait]
impl Database for PostgresDb {
    async fn init() -> Result<Self> {
        Self::new(&crate::core::config::DATABASE_URL).await
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_video_record_serialization() {
        let record = VideoRecord {
            id: 1,
            uuid: "test-uuid-123".to_string(),
            file_path: "/path/to/video.mp4".to_string(),
            file_name: "video.mp4".to_string(),
            duration: 120.5,
            width: 1920,
            height: 1080,
            fps: 30.0,
            probe_json: Some("{}".to_string()),
            storage: StorageStatus::default(),
            status: VideoStatus::Pending,
            user_id: None,
            job_id: None,
            created_at: "2024-01-01T00:00:00Z".to_string(),
        };

        let json = serde_json::to_string(&record).unwrap();
        assert!(json.contains("test-uuid-123"));
        assert!(json.contains("video.mp4"));
    }

    #[test]
    fn test_chunk_type_serialization() {
        let chunk_type = ChunkType::TimeBased;
        let json = serde_json::to_string(&chunk_type).unwrap();
        assert!(json.contains("time_based"));
    }

    #[test]
    fn test_chunk_deserialization() {
        let json = r#"{
            "file_id": 1,
            "uuid": "test",
            "chunk_id": "c1",
            "chunk_index": 0,
            "chunk_type": "time_based",
            "rule": "rule1",
            "start_time": 0.0,
            "end_time": 10.0,
            "fps": 30.0,
            "start_frame": 0,
            "end_frame": 300,
            "content": {},
            "frame_count": 300,
            "pre_chunk_ids": [],
            "parent_chunk_id": null,
            "child_chunk_ids": []
        }"#;

        let chunk: Chunk = serde_json::from_str(json).unwrap();
        assert_eq!(chunk.chunk_id, "c1");
        assert_eq!(chunk.chunk_type, ChunkType::TimeBased);
        assert_eq!(chunk.rule, ChunkRule::Rule1);
    }

    #[test]
    fn test_storage_status_default() {
        let status = StorageStatus::default();
        assert!(!status.fs_video);
        assert!(!status.fs_json);
    }

    #[test]
    fn test_monitor_job_status_from_db_str() {
        assert_eq!(
            MonitorJobStatus::from_db_str("pending"),
            Some(MonitorJobStatus::Pending)
        );
        assert_eq!(
            MonitorJobStatus::from_db_str("running"),
            Some(MonitorJobStatus::Running)
        );
        assert_eq!(
            MonitorJobStatus::from_db_str("completed"),
            Some(MonitorJobStatus::Completed)
        );
        assert_eq!(
            MonitorJobStatus::from_db_str("failed"),
            Some(MonitorJobStatus::Failed)
        );
        assert_eq!(
            MonitorJobStatus::from_db_str("cancelled"),
            Some(MonitorJobStatus::Cancelled)
        );
        assert_eq!(MonitorJobStatus::from_db_str("unknown"), None);
    }

    #[test]
    fn test_monitor_job_serialization() {
        use chrono::NaiveDateTime;

        let job = MonitorJob {
            id: 1,
            uuid: "test-uuid-456".to_string(),
            video_path: Some("/path/to/video.mp4".to_string()),
            status: MonitorJobStatus::Running,
            current_processor: Some("asr".to_string()),
            progress_total: 100,
            progress_current: 50,
            error_count: 0,
            last_error: None,
            started_at: Some(
                NaiveDateTime::parse_from_str("2024-01-01 10:00:00", "%Y-%m-%d %H:%M:%S").unwrap(),
            ),
            updated_at: Some(
                NaiveDateTime::parse_from_str("2024-01-01 10:05:00", "%Y-%m-%d %H:%M:%S").unwrap(),
            ),
            created_at: NaiveDateTime::parse_from_str("2024-01-01 09:55:00", "%Y-%m-%d %H:%M:%S")
                .unwrap(),
        };

        let json = serde_json::to_string(&job).unwrap();
        assert!(json.contains("test-uuid-456"));
        assert!(json.contains("running"));
        assert!(json.contains("asr"));
    }

    #[test]
    fn test_monitor_job_deserialization() {
        let json = r#"{
            "id": 2,
            "uuid": "abc123",
            "video_path": "/video/test.mp4",
            "status": "pending",
            "current_processor": null,
            "progress_total": 0,
            "progress_current": 0,
            "error_count": 0,
            "last_error": null,
            "started_at": null,
            "updated_at": null,
            "created_at": "2024-01-01T00:00:00"
        }"#;

        let job: MonitorJob = serde_json::from_str(json).unwrap();
        assert_eq!(job.uuid, "abc123");
        assert_eq!(job.status, MonitorJobStatus::Pending);
        assert!(job.current_processor.is_none());
    }

    #[test]
    fn test_monitor_job_stats_serialization() {
        let stats = MonitorJobStats {
            pending: 5,
            running: 2,
            completed: 100,
            failed: 3,
        };

        let json = serde_json::to_string(&stats).unwrap();
        assert!(json.contains("\"pending\":5"));
        assert!(json.contains("\"running\":2"));
        assert!(json.contains("\"completed\":100"));
        assert!(json.contains("\"failed\":3"));
    }

    #[test]
    fn test_bm25_result_serialization() {
        let result = Bm25Result {
            chunk_id: "sentence_001".to_string(),
            uuid: "test-uuid".to_string(),
            chunk_index: 1,
            chunk_type: "sentence".to_string(),
            start_frame: 0,
            end_frame: 150,
            fps: 30.0,
            start_time: 0.0,
            end_time: 5.0,
            text: "Hello world".to_string(),
            bm25_score: 0.75,
            parent_chunk_id: None,
            visual_stats: None,
            scene_summary: None,
        };

        let json = serde_json::to_string(&result).unwrap();
        assert!(json.contains("sentence_001"));
        assert!(json.contains("Hello world"));
        assert!(json.contains("0.75"));
    }

    #[test]
    fn test_hybrid_search_result_serialization() {
        let result = HybridSearchResult {
            chunk_id: "sentence_001".to_string(),
            uuid: "test-uuid".to_string(),
            chunk_index: 1,
            chunk_type: "sentence".to_string(),
            start_frame: 0,
            end_frame: 150,
            fps: 30.0,
            start_time: 0.0,
            end_time: 5.0,
            text: "Hello world".to_string(),
            vector_score: 0.85,
            bm25_score: 0.75,
            combined_score: 0.80,
            parent_chunk_id: None,
            visual_stats: None,
        };

        let json = serde_json::to_string(&result).unwrap();
        assert!(json.contains("sentence_001"));
        assert!(json.contains("0.85"));
        assert!(json.contains("0.75"));
        assert!(json.contains("0.8"));
    }

    fn prepare_tsquery_test_helper(query: &str) -> Result<String> {
        let words: Vec<String> = query
            .split_whitespace()
            .map(|w| {
                // 保留字母数字字符和Unicode字母字符（包括中文）
                let cleaned = w
                    .chars()
                    .filter(|c| c.is_alphanumeric() || c.is_alphabetic())
                    .collect::<String>();
                if cleaned.is_empty() {
                    String::new()
                } else {
                    format!("{}:*", cleaned.to_lowercase())
                }
            })
            .filter(|w| !w.is_empty())
            .collect();

        // 如果没有可搜索的术语，返回一个不会匹配任何内容的安全查询
        // 而不是报错，这样BM25搜索将返回空结果，但不会导致500错误
        if words.is_empty() {
            return Ok("__no_match__:*".to_string());
        }

        Ok(words.join(" & "))
    }

    #[test]
    fn test_prepare_tsquery() {
        let query = "Hello World";
        let tsquery = prepare_tsquery_test_helper(query).unwrap();
        assert!(tsquery.contains("hello:*"));
        assert!(tsquery.contains("world:*"));
        assert!(tsquery.contains(" & "));
    }

    #[test]
    fn test_prepare_tsquery_single_word() {
        let query = "search";
        let tsquery = prepare_tsquery_test_helper(query).unwrap();
        assert_eq!(tsquery, "search:*");
    }

    #[test]
    fn test_prepare_tsquery_chinese() {
        let query = "電腦 測試";
        let tsquery = prepare_tsquery_test_helper(query).unwrap();
        assert!(tsquery.contains("電腦:*"));
        assert!(tsquery.contains("測試:*"));
        assert!(tsquery.contains(" & "));
    }

    #[test]
    fn test_prepare_tsquery_empty_result() {
        let query = "   !!!   ";
        let result = prepare_tsquery_test_helper(query);
        // 现在应该返回安全查询而不是错误
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), "__no_match__:*");
    }
}
