use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;

use serde::{Deserialize, Serialize};
use thiserror::Error;
use tidaldb::entities::RelationshipType;
use tidaldb::query::retrieve::Retrieve;
use tidaldb::schema::{
    AgentPolicy, DecaySpec, EntityId, EntityKind, Schema, SchemaBuilder, Timestamp, Window,
};
use tidaldb::session::{SessionHandle, SessionSummary};
use tidaldb::{TidalDb, TidalError};

#[derive(Debug, Error)]
pub enum EngineError {
    #[error("tidaldb: {0}")]
    Tidal(#[from] TidalError),
    #[error("missing creator_id for action {action:?}")]
    MissingCreatorId { action: FeedbackAction },
    #[error("aux memory: {0}")]
    Aux(String),
}

pub type Result<T> = std::result::Result<T, EngineError>;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FeedbackAction {
    View,
    More,
    Less,
    Save,
    HideItem,
    MuteCreator,
    BlockCreator,
}

#[derive(Debug, Clone)]
pub struct FeedbackEvent {
    pub user_id: u64,
    pub item_id: u64,
    pub creator_id: Option<u64>,
    pub action: FeedbackAction,
    pub timestamp: Timestamp,
}

impl FeedbackEvent {
    #[must_use]
    pub fn now(
        user_id: u64,
        item_id: u64,
        creator_id: Option<u64>,
        action: FeedbackAction,
    ) -> Self {
        Self {
            user_id,
            item_id,
            creator_id,
            action,
            timestamp: Timestamp::now(),
        }
    }
}

#[derive(Debug, Clone)]
pub struct PersonalizationItem {
    pub item_id: u64,
    pub creator_id: u64,
    pub title: String,
    pub category: String,
    pub embedding: Option<Vec<f32>>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RetrievedItem {
    pub item_id: u64,
    pub creator_id: Option<u64>,
    pub title: Option<String>,
    pub category: Option<String>,
    pub score: f64,
}

pub trait AuxMemory: Send + Sync {
    fn remember_observation(&self, person_id: u64, observation: &str) -> Result<()>;
}

#[derive(Debug, Default)]
pub struct NoopAuxMemory;

impl AuxMemory for NoopAuxMemory {
    fn remember_observation(&self, _person_id: u64, _observation: &str) -> Result<()> {
        Ok(())
    }
}

#[cfg(feature = "synap-aux")]
pub struct SynapAuxMemory {
    base_url: String,
    api_key: String,
    client: reqwest::blocking::Client,
}

#[cfg(feature = "synap-aux")]
impl SynapAuxMemory {
    pub fn new(base_url: impl Into<String>, api_key: impl Into<String>) -> Result<Self> {
        let client = reqwest::blocking::Client::builder()
            .timeout(Duration::from_secs(10))
            .build()
            .map_err(|e| EngineError::Aux(format!("build client: {e}")))?;
        Ok(Self {
            base_url: base_url.into(),
            api_key: api_key.into(),
            client,
        })
    }
}

#[cfg(feature = "synap-aux")]
impl AuxMemory for SynapAuxMemory {
    fn remember_observation(&self, person_id: u64, observation: &str) -> Result<()> {
        let url = format!(
            "{}/api/v1/memories/remember",
            self.base_url.trim_end_matches('/')
        );
        let body = serde_json::json!({
            "content": observation,
            "confidence": 0.8,
            "memory_type": "semantic",
            "tags": ["observation", format!("person:{person_id}")]
        });

        let response = self
            .client
            .post(url)
            .header("Content-Type", "application/json")
            .header("Authorization", format!("Bearer {}", self.api_key))
            .json(&body)
            .send()
            .map_err(|e| EngineError::Aux(format!("request failed: {e}")))?;

        if !response.status().is_success() {
            let status = response.status();
            let text = response.text().unwrap_or_default();
            return Err(EngineError::Aux(format!("synap {}: {}", status, text)));
        }

        Ok(())
    }
}

pub struct IkyEngineBuilder {
    data_dir: Option<PathBuf>,
    aux_memory: Option<Arc<dyn AuxMemory>>,
}

impl IkyEngineBuilder {
    #[must_use]
    pub const fn new() -> Self {
        Self {
            data_dir: None,
            aux_memory: None,
        }
    }

    #[must_use]
    pub fn data_dir(mut self, path: impl Into<PathBuf>) -> Self {
        self.data_dir = Some(path.into());
        self
    }

    #[must_use]
    pub fn with_aux_memory(mut self, aux: Arc<dyn AuxMemory>) -> Self {
        self.aux_memory = Some(aux);
        self
    }

    pub fn open(self) -> Result<IkyEngine> {
        let schema = build_schema()?;

        let db = if let Some(path) = self.data_dir {
            std::fs::create_dir_all(&path)
                .map_err(|e| EngineError::Aux(format!("create data dir {path:?}: {e}")))?;
            TidalDb::builder()
                .with_data_dir(path)
                .with_schema(schema)
                .open()?
        } else {
            TidalDb::builder().ephemeral().with_schema(schema).open()?
        };

        Ok(IkyEngine {
            db,
            aux_memory: self.aux_memory,
        })
    }
}

impl Default for IkyEngineBuilder {
    fn default() -> Self {
        Self::new()
    }
}

pub struct IkyEngine {
    db: TidalDb,
    aux_memory: Option<Arc<dyn AuxMemory>>,
}

impl IkyEngine {
    #[must_use]
    pub const fn builder() -> IkyEngineBuilder {
        IkyEngineBuilder::new()
    }

    pub fn close(self) -> Result<()> {
        self.db.close()?;
        Ok(())
    }

    #[must_use]
    pub fn item_count(&self) -> u64 {
        self.db.item_count()
    }

    pub fn upsert_user(&self, user_id: u64, metadata: &HashMap<String, String>) -> Result<()> {
        self.db.write_user(EntityId::new(user_id), metadata)?;
        Ok(())
    }

    pub fn upsert_item(&self, item: &PersonalizationItem) -> Result<()> {
        let mut metadata = HashMap::new();
        metadata.insert("title".to_string(), item.title.clone());
        metadata.insert("category".to_string(), item.category.clone());
        metadata.insert("creator_id".to_string(), item.creator_id.to_string());
        metadata.insert("format".to_string(), "message".to_string());
        metadata.insert(
            "created_at".to_string(),
            Timestamp::now().as_nanos().to_string(),
        );

        self.db
            .write_item_with_metadata(EntityId::new(item.item_id), &metadata)?;

        if let Some(embedding) = &item.embedding {
            self.db
                .write_item_embedding(EntityId::new(item.item_id), embedding)?;
        }

        Ok(())
    }

    pub fn record_global_signal(
        &self,
        signal_type: &str,
        item_id: u64,
        weight: f64,
        timestamp: Timestamp,
    ) -> Result<()> {
        self.db
            .signal(signal_type, EntityId::new(item_id), weight, timestamp)?;
        Ok(())
    }

    pub fn record_feedback(&self, event: FeedbackEvent) -> Result<()> {
        let item_id = EntityId::new(event.item_id);

        match event.action {
            FeedbackAction::View => {
                self.db.signal_with_context(
                    "view",
                    item_id,
                    1.0,
                    event.timestamp,
                    Some(event.user_id),
                    event.creator_id,
                )?;
            }
            FeedbackAction::More => {
                self.db.signal_with_context(
                    "like",
                    item_id,
                    1.0,
                    event.timestamp,
                    Some(event.user_id),
                    event.creator_id,
                )?;
            }
            FeedbackAction::Less => {
                self.db.signal_with_context(
                    "skip",
                    item_id,
                    1.0,
                    event.timestamp,
                    Some(event.user_id),
                    None,
                )?;
            }
            FeedbackAction::Save => {
                self.db.signal_with_context(
                    "save",
                    item_id,
                    1.0,
                    event.timestamp,
                    Some(event.user_id),
                    event.creator_id,
                )?;
                #[allow(clippy::cast_possible_truncation)]
                self.db.user_state().add_save_timestamped(
                    event.user_id,
                    event.item_id as u32,
                    event.timestamp.as_nanos(),
                );
            }
            FeedbackAction::HideItem => {
                self.db.signal_with_context(
                    "hide",
                    item_id,
                    1.0,
                    event.timestamp,
                    Some(event.user_id),
                    None,
                )?;
                self.db.write_relationship(
                    EntityId::new(event.user_id),
                    RelationshipType::Hide,
                    item_id,
                    1.0,
                    event.timestamp,
                )?;
            }
            FeedbackAction::MuteCreator => {
                let creator_id = event.creator_id.ok_or(EngineError::MissingCreatorId {
                    action: event.action,
                })?;

                self.db.write_relationship(
                    EntityId::new(event.user_id),
                    RelationshipType::Mute,
                    EntityId::new(creator_id),
                    1.0,
                    event.timestamp,
                )?;

                // Current retrieval filtering enforces creator suppression via Blocks.
                self.db.write_relationship(
                    EntityId::new(event.user_id),
                    RelationshipType::Blocks,
                    EntityId::new(creator_id),
                    1.0,
                    event.timestamp,
                )?;
            }
            FeedbackAction::BlockCreator => {
                let creator_id = event.creator_id.ok_or(EngineError::MissingCreatorId {
                    action: event.action,
                })?;

                self.db.write_relationship(
                    EntityId::new(event.user_id),
                    RelationshipType::Blocks,
                    EntityId::new(creator_id),
                    1.0,
                    event.timestamp,
                )?;
            }
        }

        Ok(())
    }

    pub fn retrieve_for_user(&self, user_id: u64, limit: usize) -> Result<Vec<u64>> {
        let query = Retrieve::builder()
            .profile("for_you")
            .for_user(user_id)
            .limit(limit)
            .build()
            .map_err(|e| EngineError::Aux(e.to_string()))?;
        let results = self.db.retrieve(&query)?;
        Ok(results.items.iter().map(|i| i.entity_id.as_u64()).collect())
    }

    pub fn retrieve_for_user_items(
        &self,
        user_id: u64,
        limit: usize,
    ) -> Result<Vec<RetrievedItem>> {
        let query = Retrieve::builder()
            .profile("for_you")
            .for_user(user_id)
            .limit(limit)
            .build()
            .map_err(|e| EngineError::Aux(e.to_string()))?;
        let results = self.db.retrieve(&query)?;

        let mut out = Vec::with_capacity(results.items.len());
        for item in &results.items {
            let meta = self.db.get_item_metadata(item.entity_id)?;
            let creator_id = meta
                .as_ref()
                .and_then(|m| m.get("creator_id"))
                .and_then(|v| v.parse::<u64>().ok());
            let title = meta.as_ref().and_then(|m| m.get("title").cloned());
            let category = meta.as_ref().and_then(|m| m.get("category").cloned());
            out.push(RetrievedItem {
                item_id: item.entity_id.as_u64(),
                creator_id,
                title,
                category,
                score: item.score,
            });
        }

        Ok(out)
    }

    pub fn retrieve_global(&self, limit: usize) -> Result<Vec<u64>> {
        let query = Retrieve::builder()
            .profile("for_you")
            .limit(limit)
            .build()
            .map_err(|e| EngineError::Aux(e.to_string()))?;
        let results = self.db.retrieve(&query)?;
        Ok(results.items.iter().map(|i| i.entity_id.as_u64()).collect())
    }

    pub fn start_session(
        &self,
        user_id: u64,
        agent_id: &str,
        metadata: HashMap<String, String>,
    ) -> Result<SessionHandle> {
        let handle = self
            .db
            .start_session(user_id, agent_id, "iky_default", metadata)?;
        Ok(handle)
    }

    pub fn session_signal(
        &self,
        handle: &SessionHandle,
        signal_type: &str,
        item_id: u64,
        weight: f64,
        annotation: Option<String>,
    ) -> Result<()> {
        self.db.session_signal(
            handle,
            signal_type,
            EntityId::new(item_id),
            weight,
            Timestamp::now(),
            annotation,
        )?;
        Ok(())
    }

    pub fn close_session(&self, handle: SessionHandle) -> Result<SessionSummary> {
        Ok(self.db.close_session(handle)?)
    }

    pub fn remember_aux_observation(&self, person_id: u64, observation: &str) -> Result<()> {
        if let Some(aux) = &self.aux_memory {
            aux.remember_observation(person_id, observation)?;
        }
        Ok(())
    }

    pub fn get_item_creator(&self, item_id: u64) -> Result<Option<u64>> {
        let meta = self.db.get_item_metadata(EntityId::new(item_id))?;
        let creator = meta
            .as_ref()
            .and_then(|m| m.get("creator_id"))
            .and_then(|v| v.parse::<u64>().ok());
        Ok(creator)
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Pg1Metrics {
    pub hard_negative_leak_rate: f64,
    pub adaptation_p95_ms: u64,
    pub useful_item_uplift: f64,
    pub repeated_unwanted_rate: f64,
    pub total_refreshes_checked: usize,
}

pub fn run_pg1_eval(data_dir: &Path) -> Result<Pg1Metrics> {
    let user_id = 42_u64;
    let preferred_creators = [1_u64, 2_u64];
    let muted_creator = 6_u64;

    let engine = IkyEngine::builder().data_dir(data_dir).open()?;

    if engine.item_count() == 0 {
        seed_catalog(&engine)?;
    }

    let mut user_meta = HashMap::new();
    user_meta.insert("role".to_string(), "engineer".to_string());
    user_meta.insert("timezone".to_string(), "America/Los_Angeles".to_string());
    engine.upsert_user(user_id, &user_meta)?;

    // Baseline useful-item rate before user-specific feedback.
    let baseline = engine.retrieve_global(20)?;
    let baseline_useful = useful_rate(&engine, &baseline, &preferred_creators)?;

    // Positive feedback: make the engine learn creator-level preferences.
    for _ in 0..5 {
        for item_id in [1_u64, 2, 3, 21, 22, 23] {
            let creator_id = engine.get_item_creator(item_id)?;
            engine.record_feedback(FeedbackEvent::now(
                user_id,
                item_id,
                creator_id,
                FeedbackAction::More,
            ))?;
        }
    }

    let personalized = engine.retrieve_for_user(user_id, 20)?;
    let personalized_useful = useful_rate(&engine, &personalized, &preferred_creators)?;

    // Adaptation latency trials: write feedback, then measure the next-refresh time.
    let mut latencies_ms = Vec::new();
    for item_id in [4_u64, 24, 5, 25, 6, 26, 7, 27, 8, 28] {
        let creator_id = engine.get_item_creator(item_id)?;
        let started = std::time::Instant::now();
        engine.record_feedback(FeedbackEvent::now(
            user_id,
            item_id,
            creator_id,
            FeedbackAction::More,
        ))?;
        let _ = engine.retrieve_for_user(user_id, 20)?;
        latencies_ms.push(started.elapsed().as_millis() as u64);
    }
    latencies_ms.sort_unstable();
    let p95_idx = ((latencies_ms.len() as f64) * 0.95).ceil() as usize;
    let adaptation_p95_ms = latencies_ms
        .get(p95_idx.saturating_sub(1))
        .copied()
        .unwrap_or(0);

    // Hard negatives: hide one item and mute one creator.
    let hidden_item = 101_u64;
    engine.record_feedback(FeedbackEvent::now(
        user_id,
        hidden_item,
        engine.get_item_creator(hidden_item)?,
        FeedbackAction::HideItem,
    ))?;
    engine.record_feedback(FeedbackEvent::now(
        user_id,
        hidden_item,
        Some(muted_creator),
        FeedbackAction::MuteCreator,
    ))?;

    // Repeated-unwanted set (strong negatives).
    let unwanted = [102_u64, 103, 104, 105, 106];
    for item_id in unwanted {
        engine.record_feedback(FeedbackEvent::now(
            user_id,
            item_id,
            engine.get_item_creator(item_id)?,
            FeedbackAction::HideItem,
        ))?;
    }

    // Session path: verify session-scoped writes are accepted.
    let session = engine.start_session(user_id, "aeries", HashMap::new())?;
    engine.session_signal(
        &session,
        "view",
        1,
        1.0,
        Some("session sanity signal".to_string()),
    )?;
    let _ = engine.close_session(session)?;

    // Refresh checks before restart.
    let mut leak_count = 0_usize;
    let mut unwanted_hits = 0_usize;
    let refresh_checks = 20_usize;

    for _ in 0..refresh_checks {
        let ids = engine.retrieve_for_user(user_id, 20)?;
        let has_hidden = ids.contains(&hidden_item);
        let has_muted_creator = has_creator(&engine, &ids, muted_creator)?;
        if has_hidden || has_muted_creator {
            leak_count += 1;
        }

        unwanted_hits += ids.iter().filter(|id| unwanted.contains(id)).count();
    }

    engine.close()?;

    // Replay correctness check: reopen and run the same checks again.
    let reopened = IkyEngine::builder().data_dir(data_dir).open()?;
    for _ in 0..refresh_checks {
        let ids = reopened.retrieve_for_user(user_id, 20)?;
        let has_hidden = ids.contains(&hidden_item);
        let has_muted_creator = has_creator(&reopened, &ids, muted_creator)?;
        if has_hidden || has_muted_creator {
            leak_count += 1;
        }

        unwanted_hits += ids.iter().filter(|id| unwanted.contains(id)).count();
    }

    reopened.close()?;

    let total_checks = refresh_checks * 2;
    let hard_negative_leak_rate = leak_count as f64 / total_checks as f64;
    let repeated_unwanted_rate =
        unwanted_hits as f64 / (total_checks as f64 * unwanted.len() as f64);

    Ok(Pg1Metrics {
        hard_negative_leak_rate,
        adaptation_p95_ms,
        useful_item_uplift: personalized_useful - baseline_useful,
        repeated_unwanted_rate,
        total_refreshes_checked: total_checks,
    })
}

fn useful_rate(engine: &IkyEngine, ids: &[u64], preferred_creators: &[u64]) -> Result<f64> {
    if ids.is_empty() {
        return Ok(0.0);
    }

    let mut useful = 0_usize;
    for &item_id in ids {
        if let Some(creator) = engine.get_item_creator(item_id)?
            && preferred_creators.contains(&creator)
        {
            useful += 1;
        }
    }

    Ok(useful as f64 / ids.len() as f64)
}

fn has_creator(engine: &IkyEngine, ids: &[u64], creator_id: u64) -> Result<bool> {
    for &item_id in ids {
        if engine.get_item_creator(item_id)? == Some(creator_id) {
            return Ok(true);
        }
    }
    Ok(false)
}

fn build_schema() -> std::result::Result<Schema, TidalError> {
    let mut schema = SchemaBuilder::new();

    schema.embedding_slot("content", EntityKind::Item, 8);

    let _ = schema
        .signal(
            "view",
            EntityKind::Item,
            DecaySpec::Exponential {
                half_life: Duration::from_secs(7 * 24 * 3600),
            },
        )
        .windows(&[Window::OneHour, Window::TwentyFourHours, Window::AllTime])
        .velocity(true)
        .add();

    let _ = schema
        .signal(
            "like",
            EntityKind::Item,
            DecaySpec::Exponential {
                half_life: Duration::from_secs(30 * 24 * 3600),
            },
        )
        .windows(&[Window::AllTime])
        .velocity(false)
        .add();

    let _ = schema
        .signal(
            "share",
            EntityKind::Item,
            DecaySpec::Exponential {
                half_life: Duration::from_secs(7 * 24 * 3600),
            },
        )
        .windows(&[Window::TwentyFourHours, Window::AllTime])
        .velocity(true)
        .add();

    let _ = schema
        .signal(
            "completion",
            EntityKind::Item,
            DecaySpec::Exponential {
                half_life: Duration::from_secs(14 * 24 * 3600),
            },
        )
        .windows(&[Window::AllTime])
        .velocity(false)
        .add();

    let _ = schema
        .signal("skip", EntityKind::Item, DecaySpec::Permanent)
        .windows(&[Window::AllTime])
        .velocity(false)
        .add();

    let _ = schema
        .signal("hide", EntityKind::Item, DecaySpec::Permanent)
        .windows(&[Window::AllTime])
        .velocity(false)
        .add();

    let _ = schema
        .signal("dislike", EntityKind::Item, DecaySpec::Permanent)
        .windows(&[Window::AllTime])
        .velocity(false)
        .add();

    let _ = schema
        .signal(
            "save",
            EntityKind::Item,
            DecaySpec::Exponential {
                half_life: Duration::from_secs(90 * 24 * 3600),
            },
        )
        .windows(&[Window::AllTime])
        .velocity(false)
        .add();

    schema.session_policy(
        "iky_default",
        AgentPolicy {
            allowed_signals: vec![
                "view".to_string(),
                "like".to_string(),
                "share".to_string(),
                "completion".to_string(),
                "save".to_string(),
                "skip".to_string(),
                "hide".to_string(),
                "dislike".to_string(),
            ],
            denied_signals: vec![],
            max_session_duration: Duration::from_secs(60 * 60),
            max_signals_per_session: 1_000,
        },
    );

    schema
        .build()
        .map_err(|e| TidalError::internal("build_schema", e.to_string()))
}

fn seed_catalog(engine: &IkyEngine) -> Result<()> {
    // 6 creators x 20 items = 120 candidates.
    // Creators 1-2 represent "useful" content for the evaluation user,
    // creators 3-6 represent the baseline-heavy stream.
    let mut item_id = 1_u64;
    for creator_id in 1_u64..=6 {
        for idx in 0_u64..20 {
            let category = if creator_id <= 2 {
                "preferred"
            } else {
                "generic"
            };

            let embedding = vec![
                creator_id as f32 / 10.0,
                idx as f32 / 20.0,
                if creator_id <= 2 { 1.0 } else { 0.0 },
                if creator_id >= 5 { 1.0 } else { 0.0 },
                0.25,
                0.5,
                0.75,
                1.0,
            ];

            let item = PersonalizationItem {
                item_id,
                creator_id,
                title: format!("c{creator_id}-item-{idx}"),
                category: category.to_string(),
                embedding: Some(embedding),
            };
            engine.upsert_item(&item)?;

            // Keep global popularity flat so user-specific feedback is the
            // dominant source of ranking movement in PG1 uplift checks.
            let now = Timestamp::now();
            let global_views = 3;
            let global_likes = 1;
            let global_shares = 0;

            for _ in 0..global_views {
                engine.record_global_signal("view", item_id, 1.0, now)?;
            }
            for _ in 0..global_likes {
                engine.record_global_signal("like", item_id, 1.0, now)?;
            }
            for _ in 0..global_shares {
                engine.record_global_signal("share", item_id, 1.0, now)?;
            }

            item_id += 1;
        }
    }

    Ok(())
}
