use crate::events::{Consumer, Event};
use crate::parsers::wasot::parser::TargetType;
use crate::parsers::wasot::{MetaLine, QuotedString};
use serde_json;
use std::io::Write;

// Include the generated protobuf code
mod proto {
    include!(concat!(env!("OUT_DIR"), "/wasot.rs"));
}

use proto::*;

/// A listener that converts Wasot events to protobuf format and outputs them as JSON
pub struct ProtobufConsumer<W: Write> {
    writer: W,
}

impl ProtobufConsumer<std::io::Stdout> {
    /// Create a new protobuf listener that writes to stdout
    pub fn new() -> Self {
        Self {
            writer: std::io::stdout(),
        }
    }
}

impl<W: Write> ProtobufConsumer<W> {
    /// Create a new protobuf listener with a custom writer
    pub fn new_with_writer(writer: W) -> Self {
        Self { writer }
    }

    /// Convert a Wasot event to protobuf format and output as JSON
    pub fn convert_and_output(&mut self, event: &Event) {
        let _proto_event = self.convert_event(event);

        // For now, just write a simple placeholder - the proto doesn't have serde
        // TODO: Properly implement protobuf binary serialization or add serde support
        let simple_json = match event {
            Event::BlockStarted { meta_line } => {
                serde_json::json!({
                    "type": "BlockStarted",
                    "meta_line": {
                        "type_name": meta_line.type_name,
                        "level": meta_line.level
                    }
                })
            }
            Event::BlockFinished => {
                serde_json::json!({
                    "type": "BlockFinished"
                })
            }
            Event::TextFinished { lines, .. } => {
                serde_json::json!({
                    "type": "TextFinished",
                    "lines": lines
                })
            }
            Event::ContextLine { meta_line } => {
                serde_json::json!({
                    "type": "ContextLine",
                    "meta_line": {
                        "type_name": meta_line.type_name,
                        "level": meta_line.level
                    }
                })
            }
            Event::VoidElement { meta_line } => {
                serde_json::json!({
                    "type": "VoidElement",
                    "meta_line": {
                        "type_name": meta_line.type_name,
                        "level": meta_line.level,
                        "is_void": true
                    }
                })
            }
        };

        if let Err(e) = writeln!(self.writer, "{simple_json}") {
            eprintln!("Error writing JSON output: {e}");
        }
        if let Err(e) = self.writer.flush() {
            eprintln!("Error flushing output: {e}");
        }
    }

    /// Convert a Wasot ElementEvent to a protobuf WasotEvent
    fn convert_event(&self, event: &Event) -> WasotEvent {
        match event {
            Event::BlockStarted { meta_line } => WasotEvent {
                event: Some(wasot_event::Event::BlockStarted(BlockStartedEvent {
                    meta_line: Some(self.convert_meta_line(meta_line)),
                })),
            },
            Event::BlockFinished => WasotEvent {
                event: Some(wasot_event::Event::BlockFinished(BlockFinishedEvent {})),
            },
            Event::TextFinished {
                lines,
                links,
                footnotes,
            } => {
                // Convert footnotes to protobuf format
                let proto_footnotes = footnotes
                    .iter()
                    .map(|f| proto::Footnote {
                        marker: f.marker,
                        link: f.link.clone(),
                    })
                    .collect();

                // Convert links to protobuf format
                let proto_links = links
                    .iter()
                    .map(|line_links| LinksPerLine {
                        links: line_links
                            .iter()
                            .map(|link| self.convert_link(link))
                            .collect(),
                    })
                    .collect();

                WasotEvent {
                    event: Some(wasot_event::Event::TextFinished(TextEvent {
                        lines: lines.clone(),
                        links: proto_links,
                        footnotes: proto_footnotes,
                    })),
                }
            }
            Event::ContextLine { meta_line } => WasotEvent {
                event: Some(wasot_event::Event::ContextLine(proto::ContextLineEvent {
                    meta_line: Some(self.convert_meta_line(meta_line)),
                })),
            },
            Event::VoidElement { meta_line } => {
                // For now, emit VoidElement as BlockStarted (until protobuf schema is updated)
                WasotEvent {
                    event: Some(wasot_event::Event::BlockStarted(BlockStartedEvent {
                        meta_line: Some(self.convert_meta_line(meta_line)),
                    })),
                }
            }
        }
    }

    /// Convert a Wasot MetaLine to protobuf format
    fn convert_meta_line(&self, meta_line: &MetaLine) -> proto::MetaLine {
        proto::MetaLine {
            type_name: meta_line.type_name.clone(),
            level: meta_line.level as u32,
            line_index: meta_line.line_index.map(|idx| idx as u32),
            inline_text: Some(self.convert_quoted_text(&meta_line.inline_text)),
            arguments: Some(self.convert_parsed_params(&meta_line.arguments)),
            block_id: if meta_line.id.is_empty() {
                None
            } else {
                Some(meta_line.id.as_str().to_string())
            },
            is_void: meta_line.is_void,
        }
    }

    /// Convert ParsedArguments to protobuf Arguments format
    fn convert_parsed_params(
        &self,
        params: &crate::parsers::wasot::ParsedArguments,
    ) -> proto::Arguments {
        proto::Arguments {
            inline_text: Some(QuotedText {
                content: String::new(), // ParsedArguments doesn't have inline_text
                quoted: false,
                links: Vec::new(), // ParsedArguments doesn't have inline_text links
            }),
            args: params
                .args
                .iter()
                .map(|arg| self.convert_quoted_text(arg))
                .collect(),
            kwargs: params
                .kwargs
                .iter()
                .map(|(key, value)| KeyValuePair {
                    key: Some(self.convert_quoted_text(key)),
                    value: Some(self.convert_quoted_text(value)),
                })
                .collect(),
        }
    }

    /// Convert QuotedString to protobuf QuotedText
    fn convert_quoted_text(&self, quoted_string: &QuotedString) -> QuotedText {
        QuotedText {
            content: quoted_string.content.clone(),
            quoted: quoted_string.quoted,
            links: quoted_string
                .links
                .iter()
                .map(|link| self.convert_link(link))
                .collect(),
        }
    }

    /// Convert a Wasot Link to protobuf format
    fn convert_link(&self, link: &crate::parsers::wasot::Link) -> proto::Link {
        let (target_type, document_ref) = match &link.target_type {
            TargetType::Uri => (proto::TargetType::Uri as i32, None),
            TargetType::FootNote => (proto::TargetType::Footnote as i32, None),
            TargetType::Id => (proto::TargetType::Id as i32, None),
            TargetType::IdInDocument(doc_ref) => (
                proto::TargetType::IdInDocument as i32,
                Some(proto::DocumentIdReference {
                    document_id: doc_ref.document_id.value.clone(),
                    local_id: doc_ref.local_id.value.clone(),
                    document_path: doc_ref.document_path.clone(),
                }),
            ),
        };

        proto::Link {
            name: link
                .name
                .as_ref()
                .map(|name| self.convert_quoted_text(name)),
            target: link.target.clone(),
            arguments: Some(self.convert_parsed_params(&link.arguments)),
            target_type,
            start_index: link.start_index as u32,
            length: link.length as u32,
            document_ref,
        }
    }
}

impl Default for ProtobufConsumer<std::io::Stdout> {
    fn default() -> Self {
        Self::new()
    }
}

impl Consumer for ProtobufConsumer<std::io::Stdout> {
    fn handle(&mut self, event: Event) {
        self.convert_and_output(&event);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::parsers::wasot::{Id, ParsedArguments, QuotedString};

    #[test]
    fn test_new_constructor() {
        let _consumer = ProtobufConsumer::new();
        // Just verify the constructor creates the struct properly
        // We can't test stdout directly, but we can verify it compiles and doesn't panic
    }

    #[test]
    fn test_new_with_writer() {
        let buffer = Vec::new();
        let consumer = ProtobufConsumer::new_with_writer(buffer);
        // Just verify the constructor creates the struct properly
        assert_eq!(consumer.writer.len(), 0);
    }

    #[test]
    fn test_convert_and_output_block_started() {
        let mut buffer = Vec::new();
        let mut consumer = ProtobufConsumer::new_with_writer(&mut buffer);

        let meta_line = MetaLine {
            type_name: "paragraph".to_string(),
            level: 1,
            line_index: Some(1),
            is_void: false,
            inline_text: QuotedString::new("Hello".to_string(), false),
            arguments: ParsedArguments {
                args: vec![],
                kwargs: vec![],
            },
            id: Id::new("test123".to_string()),
        };

        let event = Event::BlockStarted { meta_line };
        consumer.convert_and_output(&event);

        // Verify output was written
        assert!(!buffer.is_empty());
        let output = String::from_utf8(buffer).unwrap();
        assert!(output.contains("BlockStarted"));
        assert!(output.contains("paragraph"));
    }

    #[test]
    fn test_convert_and_output_block_finished() {
        let mut buffer = Vec::new();
        let mut consumer = ProtobufConsumer::new_with_writer(&mut buffer);

        let event = Event::BlockFinished;
        consumer.convert_and_output(&event);

        // Verify output was written
        assert!(!buffer.is_empty());
        let output = String::from_utf8(buffer).unwrap();
        assert!(output.contains("BlockFinished"));
    }

    #[test]
    fn test_convert_and_output_text_finished() {
        let mut buffer = Vec::new();
        let mut consumer = ProtobufConsumer::new_with_writer(&mut buffer);

        use crate::parsers::wasot::Footnote;
        let event = Event::TextFinished {
            lines: vec!["Line 1".to_string(), "Line 2".to_string()],
            links: vec![Vec::new(), Vec::new()],
            footnotes: vec![Footnote {
                marker: 1,
                link: "http://example.com".to_string(),
            }],
        };
        consumer.convert_and_output(&event);

        // Verify output was written
        assert!(!buffer.is_empty());
        let output = String::from_utf8(buffer).unwrap();
        assert!(output.contains("TextFinished"));
        assert!(output.contains("Line 1"));
        assert!(output.contains("Line 2"));
    }

    #[test]
    fn test_convert_event_all_types() {
        let consumer = ProtobufConsumer::new_with_writer(Vec::new());

        // Test BlockStarted conversion
        let meta_line = MetaLine {
            type_name: "div".to_string(),
            level: 2,
            line_index: Some(10),
            is_void: false,
            inline_text: QuotedString::new("content".to_string(), true),
            arguments: ParsedArguments {
                args: vec![QuotedString::new("arg1".to_string(), false)],
                kwargs: vec![(
                    QuotedString::new("key".to_string(), false),
                    QuotedString::new("value".to_string(), true),
                )],
            },
            id: Id::new("my_id".to_string()),
        };
        let block_event = Event::BlockStarted { meta_line };
        let proto_event = consumer.convert_event(&block_event);
        assert!(matches!(
            proto_event.event,
            Some(wasot_event::Event::BlockStarted(_))
        ));

        // Test BlockFinished conversion
        let finish_event = Event::BlockFinished;
        let proto_event = consumer.convert_event(&finish_event);
        assert!(matches!(
            proto_event.event,
            Some(wasot_event::Event::BlockFinished(_))
        ));

        // Test TextFinished conversion
        use crate::parsers::wasot::Footnote;
        let text_event = Event::TextFinished {
            lines: vec!["line1".to_string(), "line2".to_string()],
            links: vec![Vec::new(), Vec::new()],
            footnotes: vec![
                Footnote {
                    marker: 1,
                    link: "http://example.com".to_string(),
                },
                Footnote {
                    marker: 2,
                    link: "http://example2.com".to_string(),
                },
            ],
        };
        let proto_event = consumer.convert_event(&text_event);
        if let Some(wasot_event::Event::TextFinished(text)) = proto_event.event {
            assert_eq!(text.lines.len(), 2);
            assert_eq!(text.footnotes.len(), 2);
            assert_eq!(text.footnotes[0].marker, 1);
            assert_eq!(text.footnotes[0].link, "http://example.com");
        } else {
            panic!("Expected TextFinished event");
        }
    }

    #[test]
    fn test_convert_meta_line() {
        let consumer = ProtobufConsumer::new_with_writer(Vec::new());

        // Test with all fields populated
        let meta_line = MetaLine {
            type_name: "section".to_string(),
            level: 3,
            line_index: Some(123),
            is_void: false,
            inline_text: QuotedString::new("inline content".to_string(), true),
            arguments: ParsedArguments {
                args: vec![QuotedString::new("arg".to_string(), false)],
                kwargs: vec![(
                    QuotedString::new("param".to_string(), false),
                    QuotedString::new("val".to_string(), true),
                )],
            },
            id: Id::new("my_id".to_string()),
        };

        let proto_meta = consumer.convert_meta_line(&meta_line);
        assert_eq!(proto_meta.type_name, "section");
        assert_eq!(proto_meta.level, 3);
        assert_eq!(proto_meta.line_index, Some(123));
        assert_eq!(proto_meta.block_id, Some("my_id".to_string()));

        // Test with empty ID
        let meta_line_no_id = MetaLine {
            type_name: "div".to_string(),
            level: 1,
            line_index: None,
            inline_text: QuotedString::new(String::new(), false),
            arguments: ParsedArguments {
                args: vec![],
                kwargs: vec![],
            },
            id: Id::empty(),
            is_void: false,
        };

        let proto_meta_no_id = consumer.convert_meta_line(&meta_line_no_id);
        assert_eq!(proto_meta_no_id.block_id, None);
        assert_eq!(proto_meta_no_id.line_index, None);
    }

    #[test]
    fn test_convert_parsed_params() {
        let consumer = ProtobufConsumer::new_with_writer(Vec::new());

        let params = ParsedArguments {
            args: vec![
                QuotedString::new("first".to_string(), false),
                QuotedString::new("second".to_string(), true),
            ],
            kwargs: vec![
                (
                    QuotedString::new("key1".to_string(), false),
                    QuotedString::new("value1".to_string(), false),
                ),
                (
                    QuotedString::new("key2".to_string(), false),
                    QuotedString::new("value2".to_string(), true),
                ),
            ],
        };

        let proto_args = consumer.convert_parsed_params(&params);
        assert_eq!(proto_args.args.len(), 2);
        assert_eq!(proto_args.kwargs.len(), 2);

        // Check args conversion
        assert_eq!(proto_args.args[0].content, "first");
        assert!(!proto_args.args[0].quoted);
        assert_eq!(proto_args.args[1].content, "second");
        assert!(proto_args.args[1].quoted);

        // Check kwargs conversion
        assert_eq!(proto_args.kwargs[0].key.as_ref().unwrap().content, "key1");
        assert_eq!(
            proto_args.kwargs[0].value.as_ref().unwrap().content,
            "value1"
        );
        assert!(!proto_args.kwargs[0].value.as_ref().unwrap().quoted);

        assert_eq!(proto_args.kwargs[1].key.as_ref().unwrap().content, "key2");
        assert_eq!(
            proto_args.kwargs[1].value.as_ref().unwrap().content,
            "value2"
        );
        assert!(proto_args.kwargs[1].value.as_ref().unwrap().quoted);

        // Check that inline_text is set with empty content
        assert!(proto_args.inline_text.is_some());
        assert!(proto_args.inline_text.as_ref().unwrap().content.is_empty());
        assert!(!proto_args.inline_text.as_ref().unwrap().quoted);
    }

    #[test]
    fn test_convert_quoted_text() {
        let consumer = ProtobufConsumer::new_with_writer(Vec::new());

        // Test unquoted string
        let unquoted = QuotedString::new("plain text".to_string(), false);
        let proto_unquoted = consumer.convert_quoted_text(&unquoted);
        assert_eq!(proto_unquoted.content, "plain text");
        assert!(!proto_unquoted.quoted);

        // Test quoted string
        let quoted = QuotedString::new("quoted text".to_string(), true);
        let proto_quoted = consumer.convert_quoted_text(&quoted);
        assert_eq!(proto_quoted.content, "quoted text");
        assert!(proto_quoted.quoted);

        // Links should be empty as per TODO comment
        assert!(proto_quoted.links.is_empty());
        assert!(proto_unquoted.links.is_empty());
    }

    #[test]
    fn test_default_implementation() {
        let _consumer = ProtobufConsumer::default();
        // Just verify default creates the same as new()
        // We can't directly compare them since they contain stdout
        // but we can verify the method compiles and doesn't panic
    }

    #[test]
    fn test_consumer_trait_handle() {
        let mut consumer = ProtobufConsumer::new();

        let meta_line = MetaLine {
            type_name: "test".to_string(),
            level: 1,
            line_index: Some(1),
            is_void: false,
            inline_text: QuotedString::new(String::new(), false),
            arguments: ParsedArguments {
                args: vec![],
                kwargs: vec![],
            },
            id: Id::empty(),
        };
        let event = Event::BlockStarted { meta_line };

        // This should not panic - just testing the handle method compiles and runs
        consumer.handle(event);
    }

    #[test]
    fn test_convert_link() {
        use crate::parsers::wasot::parser::TargetType;

        let consumer = ProtobufConsumer::new_with_writer(Vec::new());

        let link = crate::parsers::wasot::Link {
            name: Some(QuotedString::new("Example".to_string(), false)),
            target: Some("https://example.com".to_string()),
            target_type: TargetType::Uri,
            arguments: ParsedArguments {
                args: vec![],
                kwargs: vec![],
            },
            start_index: 0,
            length: 20,
        };

        let proto_link = consumer.convert_link(&link);

        assert!(proto_link.name.is_some());
        assert_eq!(proto_link.name.as_ref().unwrap().content, "Example");
        assert_eq!(proto_link.target, Some("https://example.com".to_string()));
        assert_eq!(proto_link.start_index, 0);
        assert_eq!(proto_link.length, 20);
        assert_eq!(proto_link.target_type, proto::TargetType::Uri as i32);
    }

    #[test]
    fn test_text_finished_with_links() {
        use crate::parsers::wasot::parser::TargetType;

        let mut buffer = Vec::new();
        let mut consumer = ProtobufConsumer::new_with_writer(&mut buffer);

        let link = crate::parsers::wasot::Link {
            name: Some(QuotedString::new("test".to_string(), false)),
            target: Some("http://test.com".to_string()),
            target_type: TargetType::Uri,
            arguments: ParsedArguments {
                args: vec![],
                kwargs: vec![],
            },
            start_index: 5,
            length: 10,
        };

        let event = Event::TextFinished {
            lines: vec!["Hello [test](http://test.com) world".to_string()],
            links: vec![vec![link]],
            footnotes: vec![],
        };

        consumer.convert_and_output(&event);

        let output = String::from_utf8(buffer).unwrap();

        // Check that the output contains the link data
        assert!(output.contains("test"));
        assert!(output.contains("http://test.com"));
        assert!(output.contains("TextFinished"));
    }

    #[test]
    fn test_convert_and_output_error_handling() {
        // Test with a writer that will cause write errors
        struct FailingWriter;

        impl Write for FailingWriter {
            fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
                Err(std::io::Error::new(
                    std::io::ErrorKind::BrokenPipe,
                    "Test error",
                ))
            }

            fn flush(&mut self) -> std::io::Result<()> {
                Err(std::io::Error::new(
                    std::io::ErrorKind::BrokenPipe,
                    "Test flush error",
                ))
            }
        }

        let mut consumer = ProtobufConsumer::new_with_writer(FailingWriter);
        let event = Event::BlockFinished;

        // This should handle errors gracefully without panicking
        consumer.convert_and_output(&event);
    }
}
