use crate::actions::Action;
use crate::app::App;
use crate::popups::{ImportSource, ImportTarget};
use anyhow::{Context, Result, bail};
use std::borrow::Cow;
use std::fs;
use std::io::Cursor;
use std::path::Path;
use std::process::{Command, Stdio};
use tempfile::NamedTempFile;

pub struct ImportAction {
    pub source: ImportSource,
    pub target: ImportTarget,
}

impl Action for ImportAction {
    fn id(&self) -> Cow<'static, str> {
        match (self.source, self.target) {
            (ImportSource::File, ImportTarget::NewNote) => Cow::Borrowed("insert.file_new"),
            (ImportSource::File, ImportTarget::AppendCurrent) => {
                Cow::Borrowed("insert.file_append")
            }
            (ImportSource::Csv, ImportTarget::NewNote) => Cow::Borrowed("insert.csv_new"),
            (ImportSource::Csv, ImportTarget::AppendCurrent) => Cow::Borrowed("insert.csv_append"),
            (ImportSource::Json, ImportTarget::NewNote) => Cow::Borrowed("insert.json_new"),
            (ImportSource::Json, ImportTarget::AppendCurrent) => {
                Cow::Borrowed("insert.json_append")
            }
            (ImportSource::Url, ImportTarget::NewNote) => Cow::Borrowed("insert.url_new"),
            (ImportSource::Url, ImportTarget::AppendCurrent) => Cow::Borrowed("insert.url_append"),
            (ImportSource::Clipboard, ImportTarget::NewNote) => {
                Cow::Borrowed("insert.clipboard_new")
            }
            (ImportSource::Clipboard, ImportTarget::AppendCurrent) => {
                Cow::Borrowed("insert.clipboard_append")
            }
        }
    }

    fn name(&self) -> Cow<'static, str> {
        let name = match self.source {
            ImportSource::File => "File",
            ImportSource::Csv => "CSV/TSV",
            ImportSource::Json => "JSON",
            ImportSource::Url => "URL",
            ImportSource::Clipboard => "Clipboard",
        };
        let action = match self.target {
            ImportTarget::NewNote => "Import",
            ImportTarget::AppendCurrent => "Append",
        };
        let suffix = match self.target {
            ImportTarget::NewNote => "as Note",
            ImportTarget::AppendCurrent => "to Note",
        };
        Cow::Owned(format!("{action} {name} {suffix}"))
    }

    fn description(&self) -> Cow<'static, str> {
        let source_desc = match self.source {
            ImportSource::File => "a file (PDF, DOCX, HTML…)",
            ImportSource::Csv => "a CSV/TSV file",
            ImportSource::Json => "a JSON file",
            ImportSource::Url => "a URL",
            ImportSource::Clipboard => "clipboard text",
        };
        let target_desc = match self.target {
            ImportTarget::NewNote => "create a note",
            ImportTarget::AppendCurrent => "append to the current note",
        };
        Cow::Owned(format!("Convert {source_desc} and {target_desc}"))
    }
    fn category(&self) -> crate::actions::ActionCategory {
        match self.target {
            ImportTarget::NewNote => crate::actions::ActionCategory::Import,
            ImportTarget::AppendCurrent => crate::actions::ActionCategory::Append,
        }
    }

    fn glyph(&self) -> (&'static str, &'static str) {
        match self.source {
            ImportSource::File => ("\u{f15b}", "\u{1f4c4}"),
            ImportSource::Csv => ("\u{f0ce}", "\u{1f4c5}"),
            ImportSource::Json => ("\u{f121}", "\u{1f4cb}"),
            ImportSource::Url => ("\u{f0ac}", "\u{1f310}"),
            ImportSource::Clipboard => ("\u{f0ea}", "\u{1f4cb}"),
        }
    }

    fn execute(&self, app: &mut App, context_note_id: Option<&str>) -> Result<()> {
        let note_id = match self.target {
            ImportTarget::AppendCurrent => {
                let id = context_note_id
                    .map(|s| s.to_string())
                    .or_else(|| app.get_selected_note_id());
                if id.is_none() {
                    app.set_temporary_status_static("Select a note first");
                    return Ok(());
                }
                id
            }
            ImportTarget::NewNote => None,
        };

        match self.source {
            ImportSource::Clipboard => {
                let (title, md) = clipboard_to_md()?;
                app.insert_content(self.target, note_id.as_deref(), title, md)?;
            }
            ImportSource::Url => {
                let folder = app.get_current_folder_context();
                app.begin_import(self.source, self.target, folder, note_id);
            }
            _ => {
                let (filter_name, filter_ext) = match self.source {
                    ImportSource::File => ("All Files", ".*"),
                    ImportSource::Csv => ("CSV Files", ".csv"),
                    ImportSource::Json => ("JSON Files", ".json"),
                    _ => unreachable!(),
                };

                if let Some(path) = crate::ui::pick_file(filter_name, filter_ext)? {
                    let result = match self.source {
                        ImportSource::File => convert_file(&path),
                        ImportSource::Csv => convert_csv(&path),
                        ImportSource::Json => convert_json(&path),
                        _ => unreachable!(),
                    };

                    match result {
                        Ok((title, md)) => {
                            app.insert_content(self.target, note_id.as_deref(), title, md)?;
                        }
                        Err(e) => {
                            app.set_temporary_status(&format!("Import failed: {e:#}"));
                            app.messages.push(
                                format!("Import failed: {e:#}"),
                                crate::app::messages::MessageSeverity::Warning,
                            );
                        }
                    }
                }
            }
        }

        Ok(())
    }
}

fn extract_html_title(html: &str) -> Option<String> {
    let mut cursor = 0;
    while let Some(start_bracket) = html[cursor..].find('<') {
        let absolute_start = cursor + start_bracket;
        let rest = &html[absolute_start..];

        if rest.len() >= 6 && rest[..6].eq_ignore_ascii_case("<title") {
            let next_char = rest.as_bytes().get(6);
            if (next_char.is_none()
                || matches!(next_char, Some(b' ' | b'>' | b'\t' | b'\r' | b'\n')))
                && let Some(tag_end_offset) = rest.find('>')
            {
                let after_tag_start = absolute_start + tag_end_offset + 1;
                let after_tag = &html[after_tag_start..];

                let mut close_cursor = 0;
                while let Some(close_bracket_offset) = after_tag[close_cursor..].find('<') {
                    let absolute_close_start = close_cursor + close_bracket_offset;
                    let close_rest = &after_tag[absolute_close_start..];
                    if close_rest.len() >= 8 && close_rest[..8].eq_ignore_ascii_case("</title>") {
                        let inner_text = &after_tag[..absolute_close_start];
                        return Some(inner_text.trim().to_string());
                    }
                    close_cursor = absolute_close_start + 1;
                }
                cursor = after_tag_start;
                continue;
            }
        }
        cursor = absolute_start + 1;
    }
    None
}

fn url_fallback_title(url: &str) -> String {
    url.trim_start_matches("http://")
        .trim_start_matches("https://")
        .split('/')
        .next()
        .unwrap_or("Imported URL")
        .to_string()
}
pub fn file_stem_title(path: &str) -> String {
    Path::new(path)
        .file_stem()
        .and_then(|s| s.to_str())
        .map(|s| {
            let mut title = s.replace(['_', '-'], " ");
            if let Some(first) = title.get_mut(0..1) {
                first.make_ascii_uppercase();
            }
            title
        })
        .unwrap_or_else(|| "Imported Note".to_string())
}

fn sanitize_import_content(content: String) -> String {
    if !content
        .chars()
        .any(|ch| ch == '\r' || (ch.is_control() && !matches!(ch, '\n' | '\t')))
    {
        return content;
    }

    let mut sanitized = String::with_capacity(content.len());
    let mut chars = content.chars().peekable();
    while let Some(ch) = chars.next() {
        match ch {
            '\r' => {
                if chars.peek() == Some(&'\n') {
                    chars.next();
                }
                sanitized.push('\n');
            }
            '\n' | '\t' => sanitized.push(ch),
            _ if ch.is_control() => {}
            _ => sanitized.push(ch),
        }
    }
    sanitized
}

fn sanitized(title: String, content: String) -> (String, String) {
    (
        crate::sanitize::sanitize_for_terminal(&title).into_owned(),
        sanitize_import_content(content),
    )
}

pub fn convert_file(path: &str) -> Result<(String, String)> {
    let mut cmd = if which::which("markitdown").is_ok() {
        let mut c = Command::new("markitdown");
        c.arg(path);
        c
    } else if which::which("pandoc").is_ok() {
        let mut c = Command::new("pandoc");
        // If it looks like HTML, tell pandoc explicitly to avoid raw HTML output
        if path.to_lowercase().ends_with(".html") || path.to_lowercase().ends_with(".htm") {
            c.args(["-f", "html", "-t", "gfm", path]);
        } else {
            c.args(["-t", "gfm", path]);
        }
        c
    } else {
        bail!(
            "markitdown or pandoc is required. Install one: pip install markitdown  (or)  install pandoc"
        );
    };

    let output = cmd
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .context("Failed to run conversion tool")?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        bail!("Conversion failed: {stderr}");
    }

    let md = String::from_utf8_lossy(&output.stdout).trim().to_string();
    if md.is_empty() {
        bail!("Conversion produced no content");
    }

    Ok(sanitized(file_stem_title(path), md))
}

fn detect_delimiter(first_line: &str) -> u8 {
    let comma = first_line.chars().filter(|&c| c == ',').count();
    let semi = first_line.chars().filter(|&c| c == ';').count();
    let tab = first_line.chars().filter(|&c| c == '\t').count();

    if tab >= comma && tab >= semi && tab > 0 {
        b'\t'
    } else if semi >= comma && semi > 0 {
        b';'
    } else {
        b','
    }
}

pub fn convert_csv(path: &str) -> Result<(String, String)> {
    let text = fs::read_to_string(path).context("Failed to read CSV file")?;
    if text.trim().is_empty() {
        bail!("CSV file is empty");
    }

    let first_line = text.lines().next().unwrap_or("");
    let mut delimiter = detect_delimiter(first_line);
    if path.ends_with(".tsv") || path.ends_with(".tab") {
        delimiter = b'\t';
    }

    let mut rdr = csv::ReaderBuilder::new()
        .delimiter(delimiter)
        .has_headers(false)
        .flexible(true)
        .from_reader(Cursor::new(text.as_bytes()));

    let mut md = String::new();
    let header_len;

    if let Some(result) = rdr.records().next() {
        let record = result.context("Failed to read CSV header")?;
        header_len = record.len();
        md.push('|');
        for field in &record {
            md.push(' ');
            md.push_str(&field.replace('|', "\\|").replace('\n', " <br>"));
            md.push_str(" |");
        }
        md.push('\n');

        md.push('|');
        for _ in 0..header_len {
            md.push_str(" --- |");
        }
        md.push('\n');
    } else {
        bail!("CSV has no rows");
    }

    for result in rdr.records() {
        let record = result.context("Failed to read CSV row")?;
        md.push('|');
        for i in 0..header_len {
            md.push(' ');
            if let Some(field) = record.get(i) {
                md.push_str(&field.replace('|', "\\|").replace('\n', " <br>"));
            }
            md.push_str(" |");
        }
        md.push('\n');
    }

    Ok(sanitized(file_stem_title(path), md))
}

pub fn convert_json(path: &str) -> Result<(String, String)> {
    let text = fs::read_to_string(path).context("Failed to read JSON file")?;
    let value: serde_json::Value = serde_json::from_str(&text).context("Invalid JSON")?;

    if let Some(arr) = value.as_array()
        && !arr.is_empty()
        && arr.iter().all(|v| v.is_object())
    {
        let mut keys = Vec::new();
        for obj in arr {
            if let Some(map) = obj.as_object() {
                for key in map.keys() {
                    if !keys.contains(key) {
                        keys.push(key.clone());
                    }
                }
            }
        }

        let mut md = String::new();
        md.push('|');
        for key in &keys {
            md.push(' ');
            md.push_str(&key.replace('|', "\\|").replace('\n', " <br>"));
            md.push_str(" |");
        }
        md.push('\n');

        md.push('|');
        for _ in 0..keys.len() {
            md.push_str(" --- |");
        }
        md.push('\n');

        for obj in arr {
            md.push('|');
            if let Some(map) = obj.as_object() {
                for key in &keys {
                    md.push(' ');
                    if let Some(val) = map.get(key) {
                        let s = match val {
                            serde_json::Value::Null => "".to_string(),
                            serde_json::Value::String(s) => s.clone(),
                            _ => val.to_string(),
                        };
                        md.push_str(&s.replace('|', "\\|").replace('\n', " <br>"));
                    }
                    md.push_str(" |");
                }
            }
            md.push('\n');
        }
        return Ok(sanitized(file_stem_title(path), md));
    }

    let md = format!("```json\n{}\n```", serde_json::to_string_pretty(&value)?);
    Ok(sanitized(file_stem_title(path), md))
}

pub fn convert_url(url: &str) -> Result<(String, String)> {
    if which::which("curl").is_err() {
        bail!("curl is required to fetch URLs");
    }

    let ext = url
        .split('?')
        .next()
        .unwrap_or(url)
        .split('.')
        .next_back()
        .filter(|e| e.len() <= 5 && e.chars().all(|c| c.is_alphanumeric()))
        .map(|e| format!(".{e}"))
        .unwrap_or_else(|| ".html".to_string());

    let temp_file = NamedTempFile::with_suffix(&ext).context("Failed to create temp file")?;
    let temp_path = temp_file
        .path()
        .to_str()
        .expect("temp path is UTF-8")
        .to_string();

    let output = Command::new("curl")
        .args(["-sL", "-o", &temp_path, url])
        .stderr(Stdio::piped())
        .output()
        .context("Failed to run curl")?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        bail!("curl failed: {stderr}");
    }

    // If markitdown is available, let it handle the URL directly for better results
    if which::which("markitdown").is_ok() {
        let output = Command::new("markitdown")
            .arg(url)
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .output()
            .context("Failed to run markitdown on URL")?;

        if output.status.success() {
            let md = String::from_utf8_lossy(&output.stdout).trim().to_string();
            if !md.is_empty() {
                // Still need title extraction for the note name
                let mut title = None;
                let temp_file = NamedTempFile::new()
                    .context("Failed to create temp file for title extraction")?;
                let temp_path = temp_file
                    .path()
                    .to_str()
                    .expect("temp path is UTF-8")
                    .to_string();
                let _ = Command::new("curl")
                    .args(["-sL", "-o", &temp_path, url])
                    .status();
                if let Ok(html) = fs::read_to_string(&temp_path) {
                    title = extract_html_title(&html);
                }
                let final_title = title.unwrap_or_else(|| url_fallback_title(url));
                let ts = chrono::Local::now().format("%Y-%m-%dT%H:%M:%S%.3f");
                std::eprintln!(
                    "[{ts}] [INFO] [import] Import URL: {url} → {final_title} ({} bytes)",
                    md.len()
                );
                return Ok(sanitized(final_title, md));
            }
        }
    }

    let (_, md) = convert_file(&temp_path)?;
    let mut title = None;
    if (ext == ".html" || ext == ".htm")
        && let Ok(html) = fs::read_to_string(&temp_path)
    {
        title = extract_html_title(&html);
    }

    let title = title.unwrap_or_else(|| url_fallback_title(url));

    Ok(sanitized(title, md))
}

pub fn clipboard_to_md() -> Result<(String, String)> {
    let mut clipboard = arboard::Clipboard::new().context("Failed to open clipboard")?;
    let text = clipboard
        .get_text()
        .context("Clipboard is empty or does not contain text")?;
    if text.trim().is_empty() {
        bail!("Clipboard is empty");
    }

    let title = format!(
        "Clipboard {}",
        chrono::Local::now().format("%Y-%m-%d %H:%M")
    );
    Ok(sanitized(title, text))
}

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

    #[test]
    fn import_sanitization_preserves_structure() {
        let (title, body) = sanitized("Ti\x07tle".into(), "α\r\nβ\rγ\n\tδ\x07".into());
        assert_eq!(title, "Title");
        assert_eq!(body, "α\nβ\nγ\n\tδ");
        assert_eq!(sanitize_import_content("\nbody\n".into()), "\nbody\n");
    }

    #[test]
    fn test_csv_conversion() -> Result<()> {
        let mut tmp = NamedTempFile::new()?;
        writeln!(tmp, "a,b,c")?;
        writeln!(tmp, "1,2,3")?;
        writeln!(tmp, "4,5|x,6")?;

        let (title, md) = convert_csv(tmp.path().to_str().unwrap())?;
        assert!(title.contains("tmp"));
        assert_eq!(
            md.lines().collect::<Vec<_>>(),
            [
                "| a | b | c |",
                "| --- | --- | --- |",
                "| 1 | 2 | 3 |",
                "| 4 | 5\\|x | 6 |",
            ]
        );
        assert!(md.ends_with('\n'));
        Ok(())
    }

    #[test]
    fn test_json_table_conversion() -> Result<()> {
        let mut tmp = NamedTempFile::new()?;
        write!(
            tmp,
            "[{{\"a\": 1, \"b\": \"x\"}}, {{\"a\": 2, \"c\": true}}]"
        )?;

        let (_, md) = convert_json(tmp.path().to_str().unwrap())?;
        assert_eq!(
            md.lines().collect::<Vec<_>>(),
            [
                "| a | b | c |",
                "| --- | --- | --- |",
                "| 1 | x |  |",
                "| 2 |  | true |",
            ]
        );
        Ok(())
    }

    #[test]
    fn test_json_block_conversion() -> Result<()> {
        let mut tmp = NamedTempFile::new()?;
        write!(tmp, "{{\"not\": \"array\"}}")?;

        let (_, md) = convert_json(tmp.path().to_str().unwrap())?;
        assert_eq!(md, "```json\n{\n  \"not\": \"array\"\n}\n```");
        Ok(())
    }

    #[test]
    fn test_file_stem_title() {
        assert_eq!(file_stem_title("/path/to/my_note.txt"), "My note");
        assert_eq!(file_stem_title("simple-test"), "Simple test");
        assert_eq!(file_stem_title(".hidden"), ".hidden");
    }
}
