//! SQL file consumer that generates SQL statements for creating a database.
//!
//! This consumer produces a `.sql` file containing CREATE TABLE and INSERT statements
//! that can be piped into SQLite (or other SQL databases) to create a database.
//! Unlike the SQLite consumer, this doesn't require SQLite libraries - it just
//! generates text SQL.

use crate::events::Event;
use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::Path;

/// Represents a block in the hierarchy stack for tracking parent relationships
#[derive(Debug, Clone)]
struct BlockStackEntry {
    /// Database ID of the element (sequential, starting at 1)
    element_id: i64,
    /// Block type (e.g., "p", "div", "text")
    #[allow(dead_code)]
    block_type: String,
    /// Nesting level
    #[allow(dead_code)]
    level: usize,
}

/// SQL file consumer that writes SQL statements to create a Wasot database.
///
/// Generates SQL compatible with SQLite (and most other SQL databases).
/// The schema matches the SQLite consumer for compatibility.
pub struct SqlFileConsumer {
    writer: BufWriter<File>,
    block_stack: Vec<BlockStackEntry>,
    next_element_id: i64,
    schema_written: bool,
}

impl SqlFileConsumer {
    /// Create a new SqlFileConsumer that writes to the given file path
    pub fn new<P: AsRef<Path>>(path: P) -> Result<Self, std::io::Error> {
        let file = File::create(path)?;
        let writer = BufWriter::new(file);
        Ok(SqlFileConsumer {
            writer,
            block_stack: Vec::new(),
            next_element_id: 1,
            schema_written: false,
        })
    }

    /// Write the database schema (CREATE TABLE statements)
    fn write_schema(&mut self) -> Result<(), std::io::Error> {
        if self.schema_written {
            return Ok(());
        }

        // Note: Using standard SQL that works with SQLite
        // The schema matches sqlite_consumer.rs for compatibility
        writeln!(self.writer, "-- Wasot database schema")?;
        writeln!(self.writer, "-- Generated by wasot to-sql")?;
        writeln!(self.writer)?;

        // Performance settings for SQLite
        writeln!(self.writer, "PRAGMA journal_mode = WAL;")?;
        writeln!(self.writer, "PRAGMA synchronous = NORMAL;")?;
        writeln!(self.writer, "PRAGMA cache_size = -65536;")?;
        writeln!(self.writer, "PRAGMA mmap_size = 268435456;")?;
        writeln!(self.writer, "PRAGMA busy_timeout = 30000;")?;
        writeln!(self.writer)?;

        // Begin transaction for faster bulk inserts
        writeln!(self.writer, "BEGIN TRANSACTION;")?;
        writeln!(self.writer)?;

        // Create element table
        writeln!(
            self.writer,
            r#"CREATE TABLE IF NOT EXISTS element (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    parent_id INTEGER,
    block_id TEXT,
    args JSON,
    content TEXT,
    element_type TEXT NOT NULL,
    block_type TEXT NOT NULL,
    line_idx INTEGER,

    FOREIGN KEY (parent_id) REFERENCES element(id),
    CHECK(block_type IN ('block', 'text')),
    CHECK(id <> parent_id)
);"#
        )?;
        writeln!(self.writer)?;

        // Create tag table
        writeln!(
            self.writer,
            r#"CREATE TABLE IF NOT EXISTS tag (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT UNIQUE NOT NULL
);"#
        )?;
        writeln!(self.writer)?;

        // Create element_tag junction table
        writeln!(
            self.writer,
            r#"CREATE TABLE IF NOT EXISTS element_tag (
    element_id INTEGER NOT NULL,
    tag_id INTEGER NOT NULL,
    PRIMARY KEY (element_id, tag_id),
    FOREIGN KEY (element_id) REFERENCES element(id),
    FOREIGN KEY (tag_id) REFERENCES tag(id)
);"#
        )?;
        writeln!(self.writer)?;

        // Create ancestor_relationship table
        writeln!(
            self.writer,
            r#"CREATE TABLE IF NOT EXISTS ancestor_relationship (
    ancestor_id INTEGER NOT NULL,
    descendant_id INTEGER NOT NULL,
    PRIMARY KEY (ancestor_id, descendant_id),
    FOREIGN KEY (ancestor_id) REFERENCES element(id),
    FOREIGN KEY (descendant_id) REFERENCES element(id),
    CHECK(ancestor_id <> descendant_id)
);"#
        )?;
        writeln!(self.writer)?;

        self.schema_written = true;
        Ok(())
    }

    /// Write indexes (called at the end for better insert performance)
    fn write_indexes(&mut self) -> Result<(), std::io::Error> {
        writeln!(self.writer)?;
        writeln!(
            self.writer,
            "-- Create indexes after data insertion for better performance"
        )?;
        writeln!(
            self.writer,
            "CREATE INDEX IF NOT EXISTS idx_element_block_id ON element(block_id);"
        )?;
        writeln!(
            self.writer,
            "CREATE INDEX IF NOT EXISTS idx_element_parent_line ON element(parent_id, line_idx);"
        )?;
        writeln!(
            self.writer,
            "CREATE INDEX IF NOT EXISTS idx_element_type ON element(element_type);"
        )?;
        writeln!(
            self.writer,
            "CREATE INDEX IF NOT EXISTS idx_element_block_type ON element(block_type);"
        )?;
        writeln!(
            self.writer,
            "CREATE INDEX IF NOT EXISTS idx_element_line_idx ON element(line_idx);"
        )?;
        writeln!(
            self.writer,
            "CREATE INDEX IF NOT EXISTS idx_tag_name ON tag(name);"
        )?;
        writeln!(self.writer)?;

        // Commit transaction
        writeln!(self.writer, "COMMIT;")?;

        Ok(())
    }

    /// Escape a string for SQL (double single quotes)
    fn escape_sql_string(s: &str) -> String {
        s.replace('\'', "''")
    }

    /// Insert a block element
    fn insert_block_element(
        &mut self,
        meta_line: &crate::parsers::wasot::MetaLine,
    ) -> Result<i64, std::io::Error> {
        let element_id = self.next_element_id;
        self.next_element_id += 1;

        let parent_id = self.block_stack.last().map(|entry| entry.element_id);

        // Serialize arguments to JSON
        let args_json = serde_json::json!({
            "args": meta_line.arguments.args,
            "kwargs": meta_line.arguments.kwargs,
            "inline_text": meta_line.inline_text
        })
        .to_string();

        let block_id = if meta_line.id.value.is_empty() {
            "NULL".to_string()
        } else {
            format!("'{}'", Self::escape_sql_string(&meta_line.id.value))
        };

        let parent_id_sql = match parent_id {
            Some(id) => id.to_string(),
            None => "NULL".to_string(),
        };

        let line_idx = meta_line.line_index.map(|idx| idx as i64).unwrap_or(0);

        writeln!(
            self.writer,
            "INSERT INTO element (id, parent_id, block_id, args, content, element_type, block_type, line_idx) VALUES ({}, {}, {}, '{}', '', '{}', 'block', {});",
            element_id,
            parent_id_sql,
            block_id,
            Self::escape_sql_string(&args_json),
            Self::escape_sql_string(&meta_line.type_name),
            line_idx
        )?;

        // Insert ancestor relationships
        for ancestor in &self.block_stack {
            writeln!(
                self.writer,
                "INSERT OR IGNORE INTO ancestor_relationship (ancestor_id, descendant_id) VALUES ({}, {});",
                ancestor.element_id,
                element_id
            )?;
        }

        Ok(element_id)
    }

    /// Insert a text element
    fn insert_text_element(
        &mut self,
        line_index: usize,
        lines: &[String],
    ) -> Result<i64, std::io::Error> {
        let element_id = self.next_element_id;
        self.next_element_id += 1;

        let parent_id = self.block_stack.last().map(|entry| entry.element_id);
        let content = lines.join("\n");

        let parent_id_sql = match parent_id {
            Some(id) => id.to_string(),
            None => "NULL".to_string(),
        };

        writeln!(
            self.writer,
            "INSERT INTO element (id, parent_id, block_id, args, content, element_type, block_type, line_idx) VALUES ({}, {}, NULL, '{{}}', '{}', 'text', 'text', {});",
            element_id,
            parent_id_sql,
            Self::escape_sql_string(&content),
            line_index as i64
        )?;

        // Insert ancestor relationships
        for ancestor in &self.block_stack {
            writeln!(
                self.writer,
                "INSERT OR IGNORE INTO ancestor_relationship (ancestor_id, descendant_id) VALUES ({}, {});",
                ancestor.element_id,
                element_id
            )?;
        }

        Ok(element_id)
    }

    /// Process an event and write corresponding SQL
    pub fn process_event(&mut self, event: &Event) {
        // Write schema on first event
        if !self.schema_written {
            if let Err(e) = self.write_schema() {
                eprintln!("Error writing SQL schema: {e}");
                return;
            }
        }

        match event {
            Event::BlockStarted { meta_line } => match self.insert_block_element(meta_line) {
                Ok(element_id) => {
                    self.block_stack.push(BlockStackEntry {
                        element_id,
                        block_type: meta_line.type_name.clone(),
                        level: meta_line.level,
                    });
                }
                Err(e) => {
                    eprintln!(
                        "Error inserting block element for {}: {e}",
                        meta_line.type_name
                    );
                }
            },

            Event::TextFinished { lines, .. } => {
                if let Err(e) = self.insert_text_element(0, lines) {
                    eprintln!("Error inserting text element: {e}");
                }
            }

            Event::BlockFinished => {
                if self.block_stack.pop().is_none() {
                    eprintln!("Warning: BlockFinished event but stack is empty");
                }
            }

            Event::ContextLine { .. } => {
                // Context lines don't affect the block structure
            }

            Event::VoidElement { meta_line } => {
                // Void elements are self-closing blocks with no children
                // Insert them as a block and immediately close (don't push to stack)
                if let Err(e) = self.insert_block_element(meta_line) {
                    eprintln!(
                        "Error inserting void element for {}: {e}",
                        meta_line.type_name
                    );
                }
            }
        }
    }

    /// Finish writing and close the file
    pub fn finish(mut self) -> Result<(), std::io::Error> {
        // Write indexes at the end
        self.write_indexes()?;
        self.writer.flush()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::events::Event;
    use crate::parsers::wasot::{Id, MetaLine, ParsedArguments, QuotedString};
    use std::fs;
    use std::io::Read;

    fn create_test_meta_line(block_type: &str, level: usize) -> MetaLine {
        MetaLine {
            type_name: block_type.to_string(),
            level,
            inline_text: QuotedString::new("Test content".to_string(), false),
            id: Id::empty(),
            line_index: Some(0),
            is_void: false,
            arguments: ParsedArguments {
                args: Vec::new(),
                kwargs: Vec::new(),
            },
        }
    }

    fn read_file_contents(path: &std::path::Path) -> Result<String, std::io::Error> {
        let mut file = fs::File::open(path)?;
        let mut contents = String::new();
        file.read_to_string(&mut contents)?;
        Ok(contents)
    }

    #[test]
    fn test_sql_file_consumer_new() {
        let temp_dir = std::env::temp_dir();
        let test_file = temp_dir.join("test_sql_new.sql");

        let _ = fs::remove_file(&test_file);

        let consumer = SqlFileConsumer::new(&test_file);
        assert!(consumer.is_ok());

        let consumer = consumer.unwrap();
        assert!(consumer.block_stack.is_empty());
        assert!(!consumer.schema_written);

        let _ = fs::remove_file(&test_file);
    }

    #[test]
    fn test_sql_file_consumer_schema() {
        let temp_dir = std::env::temp_dir();
        let test_file = temp_dir.join("test_sql_schema.sql");

        let _ = fs::remove_file(&test_file);

        let mut consumer = SqlFileConsumer::new(&test_file).unwrap();

        // Process one event to trigger schema writing
        let meta_line = create_test_meta_line("paragraph", 1);
        let event = Event::BlockStarted { meta_line };
        consumer.process_event(&event);
        consumer.process_event(&Event::BlockFinished);

        consumer.finish().unwrap();

        let contents = read_file_contents(&test_file).unwrap();

        // Check schema elements
        assert!(contents.contains("CREATE TABLE IF NOT EXISTS element"));
        assert!(contents.contains("CREATE TABLE IF NOT EXISTS tag"));
        assert!(contents.contains("CREATE TABLE IF NOT EXISTS element_tag"));
        assert!(contents.contains("CREATE TABLE IF NOT EXISTS ancestor_relationship"));
        assert!(contents.contains("BEGIN TRANSACTION"));
        assert!(contents.contains("COMMIT"));

        let _ = fs::remove_file(&test_file);
    }

    #[test]
    fn test_sql_file_consumer_block_insert() {
        let temp_dir = std::env::temp_dir();
        let test_file = temp_dir.join("test_sql_block.sql");

        let _ = fs::remove_file(&test_file);

        let mut consumer = SqlFileConsumer::new(&test_file).unwrap();

        let meta_line = create_test_meta_line("paragraph", 1);
        consumer.process_event(&Event::BlockStarted { meta_line });
        consumer.process_event(&Event::BlockFinished);

        consumer.finish().unwrap();

        let contents = read_file_contents(&test_file).unwrap();

        assert!(contents.contains("INSERT INTO element"));
        assert!(contents.contains("'paragraph'"));
        assert!(contents.contains("'block'"));

        let _ = fs::remove_file(&test_file);
    }

    #[test]
    fn test_sql_file_consumer_text_insert() {
        let temp_dir = std::env::temp_dir();
        let test_file = temp_dir.join("test_sql_text.sql");

        let _ = fs::remove_file(&test_file);

        let mut consumer = SqlFileConsumer::new(&test_file).unwrap();

        let meta_line = create_test_meta_line("paragraph", 1);
        consumer.process_event(&Event::BlockStarted { meta_line });
        consumer.process_event(&Event::TextFinished {
            lines: vec!["Hello world".to_string()],
            links: vec![vec![]],
            footnotes: vec![],
        });
        consumer.process_event(&Event::BlockFinished);

        consumer.finish().unwrap();

        let contents = read_file_contents(&test_file).unwrap();

        assert!(contents.contains("'Hello world'"));
        assert!(contents.contains("'text'"));

        let _ = fs::remove_file(&test_file);
    }

    #[test]
    fn test_sql_file_consumer_nested_blocks() {
        let temp_dir = std::env::temp_dir();
        let test_file = temp_dir.join("test_sql_nested.sql");

        let _ = fs::remove_file(&test_file);

        let mut consumer = SqlFileConsumer::new(&test_file).unwrap();

        // Create nested structure: section > paragraph
        let section = create_test_meta_line("section", 1);
        consumer.process_event(&Event::BlockStarted { meta_line: section });

        let paragraph = create_test_meta_line("paragraph", 2);
        consumer.process_event(&Event::BlockStarted {
            meta_line: paragraph,
        });
        consumer.process_event(&Event::BlockFinished);
        consumer.process_event(&Event::BlockFinished);

        consumer.finish().unwrap();

        let contents = read_file_contents(&test_file).unwrap();

        // Check ancestor relationship was created
        assert!(contents.contains("INSERT OR IGNORE INTO ancestor_relationship"));

        let _ = fs::remove_file(&test_file);
    }

    #[test]
    fn test_sql_file_consumer_escape_quotes() {
        let temp_dir = std::env::temp_dir();
        let test_file = temp_dir.join("test_sql_escape.sql");

        let _ = fs::remove_file(&test_file);

        let mut consumer = SqlFileConsumer::new(&test_file).unwrap();

        let meta_line = create_test_meta_line("paragraph", 1);
        consumer.process_event(&Event::BlockStarted { meta_line });
        consumer.process_event(&Event::TextFinished {
            lines: vec!["It's a test with 'quotes'".to_string()],
            links: vec![vec![]],
            footnotes: vec![],
        });
        consumer.process_event(&Event::BlockFinished);

        consumer.finish().unwrap();

        let contents = read_file_contents(&test_file).unwrap();

        // Check quotes are properly escaped
        assert!(contents.contains("It''s a test with ''quotes''"));

        let _ = fs::remove_file(&test_file);
    }

    #[test]
    fn test_sql_file_consumer_with_block_id() {
        let temp_dir = std::env::temp_dir();
        let test_file = temp_dir.join("test_sql_block_id.sql");

        let _ = fs::remove_file(&test_file);

        let mut consumer = SqlFileConsumer::new(&test_file).unwrap();

        let mut meta_line = create_test_meta_line("paragraph", 1);
        meta_line.id = Id::new("my_block_id".to_string());

        consumer.process_event(&Event::BlockStarted { meta_line });
        consumer.process_event(&Event::BlockFinished);

        consumer.finish().unwrap();

        let contents = read_file_contents(&test_file).unwrap();

        assert!(contents.contains("'my_block_id'"));

        let _ = fs::remove_file(&test_file);
    }

    #[test]
    fn test_sql_file_consumer_indexes() {
        let temp_dir = std::env::temp_dir();
        let test_file = temp_dir.join("test_sql_indexes.sql");

        let _ = fs::remove_file(&test_file);

        let mut consumer = SqlFileConsumer::new(&test_file).unwrap();

        let meta_line = create_test_meta_line("paragraph", 1);
        consumer.process_event(&Event::BlockStarted { meta_line });
        consumer.process_event(&Event::BlockFinished);

        consumer.finish().unwrap();

        let contents = read_file_contents(&test_file).unwrap();

        // Check indexes are created
        assert!(contents.contains("CREATE INDEX IF NOT EXISTS idx_element_block_id"));
        assert!(contents.contains("CREATE INDEX IF NOT EXISTS idx_element_parent_line"));
        assert!(contents.contains("CREATE INDEX IF NOT EXISTS idx_element_type"));
        assert!(contents.contains("CREATE INDEX IF NOT EXISTS idx_tag_name"));

        let _ = fs::remove_file(&test_file);
    }
}
