//! SQLite implementation of NoteRepository with FTS5 full-text search

use async_trait::async_trait;
use chrono::{DateTime, Utc};
use sqlx::{FromRow, SqlitePool};
use uuid::Uuid;

use notes_domain::{
    DomainError, DomainResult, Note, NoteFilter, NoteRepository, NoteVersion, Tag, TagRepository,
};

use crate::tag_repository::SqliteTagRepository;

/// SQLite adapter for NoteRepository
pub struct SqliteNoteRepository {
    pool: SqlitePool,
    tag_repo: SqliteTagRepository,
}

impl SqliteNoteRepository {
    pub fn new(pool: SqlitePool) -> Self {
        let tag_repo = SqliteTagRepository::new(pool.clone());
        Self { pool, tag_repo }
    }
}

#[derive(Debug, FromRow)]
struct NoteRow {
    id: String,
    user_id: String,
    title: String,
    content: String,
    color: String,
    is_pinned: i32,
    is_archived: i32,
    created_at: String,
    updated_at: String,
}

impl NoteRow {
    fn try_into_note(self, tags: Vec<Tag>) -> Result<Note, DomainError> {
        let id = Uuid::parse_str(&self.id)
            .map_err(|e| DomainError::RepositoryError(format!("Invalid UUID: {}", e)))?;
        let user_id = Uuid::parse_str(&self.user_id)
            .map_err(|e| DomainError::RepositoryError(format!("Invalid UUID: {}", e)))?;

        let parse_datetime = |s: &str| -> Result<DateTime<Utc>, DomainError> {
            DateTime::parse_from_rfc3339(s)
                .map(|dt| dt.with_timezone(&Utc))
                .or_else(|_| {
                    chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S")
                        .map(|dt| dt.and_utc())
                })
                .map_err(|e| DomainError::RepositoryError(format!("Invalid datetime: {}", e)))
        };

        let created_at = parse_datetime(&self.created_at)?;
        let updated_at = parse_datetime(&self.updated_at)?;

        Ok(Note {
            id,
            user_id,
            title: self.title,
            content: self.content,
            color: self.color,
            is_pinned: self.is_pinned != 0,
            is_archived: self.is_archived != 0,
            created_at,
            updated_at,
            tags,
        })
    }
}

#[derive(Debug, FromRow)]
struct NoteVersionRow {
    id: String,
    note_id: String,
    title: String,
    content: String,
    created_at: String,
}

impl NoteVersionRow {
    fn try_into_version(self) -> Result<NoteVersion, DomainError> {
        let id = Uuid::parse_str(&self.id)
            .map_err(|e| DomainError::RepositoryError(format!("Invalid UUID: {}", e)))?;
        let note_id = Uuid::parse_str(&self.note_id)
            .map_err(|e| DomainError::RepositoryError(format!("Invalid UUID: {}", e)))?;

        let created_at = DateTime::parse_from_rfc3339(&self.created_at)
            .map(|dt| dt.with_timezone(&Utc))
            .or_else(|_| {
                chrono::NaiveDateTime::parse_from_str(&self.created_at, "%Y-%m-%d %H:%M:%S")
                    .map(|dt| dt.and_utc())
            })
            .map_err(|e| DomainError::RepositoryError(format!("Invalid datetime: {}", e)))?;

        Ok(NoteVersion {
            id,
            note_id,
            title: self.title,
            content: self.content,
            created_at,
        })
    }
}

#[async_trait]
impl NoteRepository for SqliteNoteRepository {
    async fn find_by_id(&self, id: Uuid) -> DomainResult<Option<Note>> {
        let id_str = id.to_string();
        let row: Option<NoteRow> = sqlx::query_as(
            r#"
            SELECT id, user_id, title, content, color, is_pinned, is_archived, created_at, updated_at
            FROM notes WHERE id = ?
            "#,
        )
        .bind(&id_str)
        .fetch_optional(&self.pool)
        .await
        .map_err(|e| DomainError::RepositoryError(e.to_string()))?;

        match row {
            Some(row) => {
                let tags = self.tag_repo.find_by_note(id).await?;
                Ok(Some(row.try_into_note(tags)?))
            }
            None => Ok(None),
        }
    }

    async fn find_by_user(&self, user_id: Uuid, filter: NoteFilter) -> DomainResult<Vec<Note>> {
        let user_id_str = user_id.to_string();

        // Build dynamic query based on filter
        let mut query = String::from(
            r#"
            SELECT id, user_id, title, content, color, is_pinned, is_archived, created_at, updated_at
            FROM notes
            WHERE user_id = ?
            "#,
        );

        if let Some(pinned) = filter.is_pinned {
            query.push_str(&format!(" AND is_pinned = {}", if pinned { 1 } else { 0 }));
        }

        if let Some(archived) = filter.is_archived {
            query.push_str(&format!(
                " AND is_archived = {}",
                if archived { 1 } else { 0 }
            ));
        }

        if let Some(tag_id) = filter.tag_id {
            query.push_str(&format!(
                " AND id IN (SELECT note_id FROM note_tags WHERE tag_id = '{}')",
                tag_id
            ));
        }

        query.push_str(" ORDER BY is_pinned DESC, updated_at DESC");

        let rows: Vec<NoteRow> = sqlx::query_as(&query)
            .bind(&user_id_str)
            .fetch_all(&self.pool)
            .await
            .map_err(|e| DomainError::RepositoryError(e.to_string()))?;

        let mut notes = Vec::with_capacity(rows.len());
        for row in rows {
            let note_id = Uuid::parse_str(&row.id)
                .map_err(|e| DomainError::RepositoryError(format!("Invalid UUID: {}", e)))?;
            let tags = self.tag_repo.find_by_note(note_id).await?;
            notes.push(row.try_into_note(tags)?);
        }

        Ok(notes)
    }

    async fn save(&self, note: &Note) -> DomainResult<()> {
        let id = note.id.to_string();
        let user_id = note.user_id.to_string();
        let is_pinned: i32 = if note.is_pinned { 1 } else { 0 };
        let is_archived: i32 = if note.is_archived { 1 } else { 0 };
        let created_at = note.created_at.to_rfc3339();
        let updated_at = note.updated_at.to_rfc3339();

        sqlx::query(
            r#"
            INSERT INTO notes (id, user_id, title, content, color, is_pinned, is_archived, created_at, updated_at)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
            ON CONFLICT(id) DO UPDATE SET
                title = excluded.title,
                content = excluded.content,
                color = excluded.color,
                is_pinned = excluded.is_pinned,
                is_archived = excluded.is_archived,
                updated_at = excluded.updated_at
            "#
        )
        .bind(&id)
        .bind(&user_id)
        .bind(&note.title)
        .bind(&note.content)
        .bind(&note.color)
        .bind(is_pinned)
        .bind(is_archived)
        .bind(&created_at)
        .bind(&updated_at)
        .execute(&self.pool)
        .await
        .map_err(|e| DomainError::RepositoryError(e.to_string()))?;

        Ok(())
    }

    async fn delete(&self, id: Uuid) -> DomainResult<()> {
        let id_str = id.to_string();
        sqlx::query("DELETE FROM notes WHERE id = ?")
            .bind(&id_str)
            .execute(&self.pool)
            .await
            .map_err(|e| DomainError::RepositoryError(e.to_string()))?;

        Ok(())
    }

    async fn search(&self, user_id: Uuid, query: &str) -> DomainResult<Vec<Note>> {
        let user_id_str = user_id.to_string();

        let like_query = format!("%{}%", query);

        // Use FTS5 for full-text search OR tag name match
        let rows: Vec<NoteRow> = sqlx::query_as(
            r#"
            SELECT DISTINCT n.id, n.user_id, n.title, n.content, n.color, n.is_pinned, n.is_archived, n.created_at, n.updated_at
            FROM notes n
            WHERE n.user_id = ? 
            AND (
                n.rowid IN (SELECT rowid FROM notes_fts WHERE notes_fts MATCH ?)
                OR
                EXISTS (
                    SELECT 1 FROM note_tags nt 
                    JOIN tags t ON nt.tag_id = t.id 
                    WHERE nt.note_id = n.id AND t.name LIKE ?
                )
            )
            ORDER BY n.updated_at DESC
            "#
        )
        .bind(&user_id_str)
        .bind(query)
        .bind(like_query)
        .fetch_all(&self.pool)
        .await
        .map_err(|e| DomainError::RepositoryError(e.to_string()))?;

        let mut notes = Vec::with_capacity(rows.len());
        for row in rows {
            let note_id = Uuid::parse_str(&row.id)
                .map_err(|e| DomainError::RepositoryError(format!("Invalid UUID: {}", e)))?;
            let tags = self.tag_repo.find_by_note(note_id).await?;
            notes.push(row.try_into_note(tags)?);
        }

        Ok(notes)
    }

    async fn save_version(&self, version: &NoteVersion) -> DomainResult<()> {
        let id = version.id.to_string();
        let note_id = version.note_id.to_string();
        let created_at = version.created_at.to_rfc3339();

        sqlx::query(
            r#"
            INSERT INTO note_versions (id, note_id, title, content, created_at)
            VALUES (?, ?, ?, ?, ?)
            "#,
        )
        .bind(&id)
        .bind(&note_id)
        .bind(&version.title)
        .bind(&version.content)
        .bind(&created_at)
        .execute(&self.pool)
        .await
        .map_err(|e| DomainError::RepositoryError(e.to_string()))?;

        Ok(())
    }

    async fn find_versions_by_note_id(&self, note_id: Uuid) -> DomainResult<Vec<NoteVersion>> {
        let note_id_str = note_id.to_string();

        let rows: Vec<NoteVersionRow> = sqlx::query_as(
            r#"
            SELECT id, note_id, title, content, created_at
            FROM note_versions
            WHERE note_id = ?
            ORDER BY created_at DESC
            "#,
        )
        .bind(&note_id_str)
        .fetch_all(&self.pool)
        .await
        .map_err(|e| DomainError::RepositoryError(e.to_string()))?;

        let mut versions = Vec::with_capacity(rows.len());
        for row in rows {
            versions.push(row.try_into_version()?);
        }

        Ok(versions)
    }
}
