use crate::api::{EnrichedNote, Note};
use crate::apis::{attachments_api, notebooks_api, notes_api, workspaces_api};
use crate::selector::NoteSelector;
use crate::services::common::{
    extract_title_from_content, get_master_key, set_file_timestamps, sluggify,
};
use anyhow::Result;
use std::fs;
use std::path::{Path, PathBuf};

pub async fn list_notes(token: &str, notebook_id: &str) -> Result<Vec<Note>> {
    notes_api::list_notes(token, notebook_id).await
}

pub async fn list_enriched_notes(token: &str) -> Result<Vec<EnrichedNote>> {
    let workspaces = workspaces_api::list_workspaces(token).await?;
    let notebooks = notebooks_api::list_notebooks(token).await?;
    let mut enriched_notes = Vec::new();

    for notebook in &notebooks {
        match list_notes(token, &notebook.id).await {
            Ok(notes) => {
                let workspace = workspaces.iter().find(|w| w.id == notebook.workspace_id);

                let workspace_name = workspace
                    .map(|w| w.name.clone())
                    .unwrap_or_else(|| "Unknown".to_string());

                let workspace_slug = workspace
                    .map(|w| w.slug.clone())
                    .unwrap_or_else(|| "unknown".to_string());

                for note in notes {
                    enriched_notes.push(EnrichedNote {
                        note,
                        notebook_name: notebook.name.clone(),
                        notebook_slug: notebook.slug.clone(),
                        workspace_name: workspace_name.clone(),
                        workspace_slug: workspace_slug.clone(),
                    });
                }
            }
            Err(e) => eprintln!(
                "Warning: Failed to fetch notes from notebook {}: {}",
                notebook.name, e
            ),
        }
    }

    Ok(enriched_notes)
}

pub async fn get_note(token: &str, notebook_id: &str, note_id: &str) -> Result<Note> {
    notes_api::get_note(token, notebook_id, note_id).await
}

pub async fn create_note(
    token: &str,
    notebook_id: &str,
    note_request: &serde_json::Value,
) -> Result<String> {
    notes_api::create_note(token, notebook_id, note_request).await
}

pub async fn update_note(
    token: &str,
    notebook_id: &str,
    note_id: &str,
    note_request: &serde_json::Value,
) -> Result<()> {
    notes_api::update_note(token, notebook_id, note_id, note_request).await
}

pub async fn delete_note(token: &str, notebook_id: &str, note_id: &str) -> Result<()> {
    notes_api::delete_note(token, notebook_id, note_id).await
}

pub async fn trash_note(token: &str, notebook_id: &str, note_id: &str) -> Result<()> {
    update_note_property(
        token,
        notebook_id,
        note_id,
        "trashed",
        serde_json::json!(true),
    )
    .await
}

pub async fn resolve_note_selector(
    token: &str,
    selector: &NoteSelector,
    note_id_hint: Option<&str>,
) -> Result<(String, String)> {
    let workspaces = workspaces_api::list_workspaces(token).await?;
    let notebooks = notebooks_api::list_notebooks(token).await?;

    let notebook = if let Some(workspace_slug) = &selector.workspace_slug {
        let workspace = workspaces
            .iter()
            .find(|w| w.slug == *workspace_slug)
            .ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", workspace_slug))?;

        notebooks
            .iter()
            .find(|n| {
                n.workspace_id == workspace.id
                    && selector
                        .notebook_slug
                        .as_ref()
                        .map_or(false, |slug| n.slug == *slug)
            })
            .ok_or_else(|| {
                anyhow::anyhow!(
                    "Notebook '{}' not found in workspace '{}'",
                    selector.notebook_slug.as_ref().unwrap(),
                    workspace_slug
                )
            })?
    } else if let Some(notebook_slug) = &selector.notebook_slug {
        notebooks
            .iter()
            .find(|n| n.slug == *notebook_slug)
            .ok_or_else(|| anyhow::anyhow!("Notebook '{}' not found", notebook_slug))?
    } else {
        anyhow::bail!("No notebook specified in selector");
    };

    if let Some(note_slug) = &selector.note_slug {
        let notes = list_notes(token, &notebook.id).await?;
        let matching_notes: Vec<_> = notes
            .iter()
            .filter(|n| n.metadata.slug == *note_slug)
            .collect();

        if matching_notes.is_empty() {
            anyhow::bail!(
                "Note '{}' not found in notebook '{}'",
                note_slug,
                notebook.slug
            );
        }

        let note = if matching_notes.len() == 1 {
            matching_notes[0]
        } else {
            if let Some(hint_id) = note_id_hint {
                if let Some(matched_note) = matching_notes.iter().find(|n| n.metadata.id == hint_id)
                {
                    matched_note
                } else {
                    anyhow::bail!(
                        "Note ID '{}' from file does not match any note with slug '{}' in notebook '{}'",
                        hint_id,
                        note_slug,
                        notebook.slug
                    );
                }
            } else {
                println!("\nMultiple notes found with slug '{}':", note_slug);
                println!();
                for (idx, note) in matching_notes.iter().enumerate() {
                    println!(
                        "  [{}] {} (ID: {}, Modified: {})",
                        idx + 1,
                        note.metadata.title,
                        note.metadata.id,
                        note.metadata.modified
                    );
                }
                println!();

                let selection = loop {
                    print!("Select a note [1-{}]: ", matching_notes.len());
                    use std::io::Write;
                    std::io::stdout().flush()?;

                    let mut input = String::new();
                    std::io::stdin().read_line(&mut input)?;

                    match input.trim().parse::<usize>() {
                        Ok(idx) if idx >= 1 && idx <= matching_notes.len() => {
                            break idx - 1;
                        }
                        _ => {
                            println!(
                                "Invalid selection. Please enter a number between 1 and {}.",
                                matching_notes.len()
                            );
                        }
                    }
                };

                matching_notes[selection]
            }
        };

        Ok((note.metadata.id.clone(), notebook.id.clone()))
    } else {
        anyhow::bail!("No note specified in selector");
    }
}

pub fn decrypt_note_content(content: &str) -> Result<String> {
    let master_key = get_master_key()?;

    match crate::crypto::decrypt(content, &master_key) {
        Ok(decrypted) => {
            println!("✓ Note decrypted successfully");
            Ok(decrypted)
        }
        Err(_) => {
            println!("→ Note is private but not encrypted");
            Ok(content.to_string())
        }
    }
}

pub fn encrypt_note_content(content: &str) -> Result<String> {
    let master_key = get_master_key()?;

    match crate::crypto::encrypt(content, &master_key) {
        Ok(encrypted) => {
            println!("✓ Note encrypted successfully");
            Ok(encrypted)
        }
        Err(e) => {
            anyhow::bail!("Failed to encrypt note: {}", e);
        }
    }
}

pub async fn save_note_to_file(
    token: &str,
    notebook_id: &str,
    note_id: &str,
    file_path: &Path,
) -> Result<()> {
    let note = get_note(token, notebook_id, note_id).await?;

    // Handle file notes separately
    if note.metadata.note_type == "file" {
        return save_file_note(token, notebook_id, &note, file_path).await;
    }

    let is_public = note.metadata.public;

    let content = if let Some(content) = note.content {
        if !is_public {
            decrypt_note_content(&content)?
        } else {
            content
        }
    } else {
        String::new()
    };

    let mut output_content = String::new();

    output_content.push_str("---\n");
    output_content.push_str(&format!("id: {}\n", note.metadata.id));
    output_content.push_str(&format!("type: {}\n", note.metadata.note_type));
    output_content.push_str(&format!("title: {}\n", note.metadata.title));
    output_content.push_str(&format!("slug: {}\n", note.metadata.slug));
    if !note.metadata.labels.is_empty() {
        output_content.push_str("labels:\n");
        for label in &note.metadata.labels {
            output_content.push_str(&format!("  - {}\n", label));
        }
    } else {
        output_content.push_str("labels: []\n");
    }
    output_content.push_str(&format!("favorite: {}\n", note.metadata.favorite));
    output_content.push_str(&format!("trashed: {}\n", note.metadata.trashed));
    output_content.push_str(&format!("archived: {}\n", note.metadata.archived));
    output_content.push_str(&format!("public: {}\n", note.metadata.public));
    output_content.push_str(&format!("created: {}\n", note.metadata.created));
    output_content.push_str(&format!("modified: {}\n", note.metadata.modified));
    output_content.push_str("---\n\n");

    output_content.push_str(&content);

    fs::write(file_path, output_content)?;

    set_file_timestamps(file_path, &note.metadata.created, &note.metadata.modified)?;

    Ok(())
}

async fn save_file_note(
    token: &str,
    notebook_id: &str,
    note: &Note,
    base_path: &Path,
) -> Result<()> {
    // Decrypt note body if private before extracting attachment ID
    let body_content = if let Some(content) = &note.content {
        if !note.metadata.public {
            decrypt_note_content(content)?
        } else {
            content.clone()
        }
    } else {
        String::new()
    };

    // Extract attachment ID from decrypted body
    let attachment_id = extract_attachment_id_from_body(&body_content)?;

    // List attachments to get file info
    let attachments =
        attachments_api::list_note_attachments(token, notebook_id, &note.metadata.id).await?;

    let attachment = attachments
        .iter()
        .find(|a| a.id.to_string() == attachment_id)
        .ok_or_else(|| anyhow::anyhow!("Attachment not found: {}", attachment_id))?;

    // Determine file extension from attachment filename or content type
    let extension = get_file_extension(&attachment.file_name, &attachment.content_type);

    // Download the attachment
    let file_data = attachments_api::download_note_attachment(
        token,
        notebook_id,
        &note.metadata.id,
        &attachment_id,
    )
    .await?;

    // Save the binary file as slug + extension
    let file_path = base_path
        .parent()
        .unwrap_or(Path::new("."))
        .join(format!("{}{}", note.metadata.slug, extension));

    fs::write(&file_path, file_data)?;

    // Save metadata in sidecar .doqium file (with decrypted body)
    let sidecar_path = base_path
        .parent()
        .unwrap_or(Path::new("."))
        .join(format!("{}.doqium", note.metadata.slug));

    save_file_note_metadata(&sidecar_path, note, &body_content, &attachment.file_name)?;

    // Set timestamps on both files
    set_file_timestamps(&file_path, &note.metadata.created, &note.metadata.modified)?;
    set_file_timestamps(
        &sidecar_path,
        &note.metadata.created,
        &note.metadata.modified,
    )?;

    Ok(())
}

fn save_file_note_metadata(
    sidecar_path: &Path,
    note: &Note,
    decrypted_content: &str,
    original_filename: &str,
) -> Result<()> {
    let content = decrypted_content.to_string();

    let mut output_content = String::new();

    output_content.push_str("---\n");
    output_content.push_str(&format!("id: {}\n", note.metadata.id));
    output_content.push_str(&format!("type: {}\n", note.metadata.note_type));
    output_content.push_str(&format!("title: {}\n", note.metadata.title));
    output_content.push_str(&format!("slug: {}\n", note.metadata.slug));
    if !note.metadata.labels.is_empty() {
        output_content.push_str("labels:\n");
        for label in &note.metadata.labels {
            output_content.push_str(&format!("  - {}\n", label));
        }
    } else {
        output_content.push_str("labels: []\n");
    }
    output_content.push_str(&format!("favorite: {}\n", note.metadata.favorite));
    output_content.push_str(&format!("trashed: {}\n", note.metadata.trashed));
    output_content.push_str(&format!("archived: {}\n", note.metadata.archived));
    output_content.push_str(&format!("public: {}\n", note.metadata.public));
    output_content.push_str(&format!("created: {}\n", note.metadata.created));
    output_content.push_str(&format!("modified: {}\n", note.metadata.modified));
    output_content.push_str(&format!("originalFileName: {}\n", original_filename));
    output_content.push_str("---\n\n");

    output_content.push_str(&content);

    fs::write(sidecar_path, output_content)?;

    Ok(())
}

fn extract_attachment_id_from_body(content: &str) -> Result<String> {
    // Parse the frontmatter in the body to extract attachmentId
    let (frontmatter, _) = crate::services::common::parse_markdown_frontmatter(content)?;

    frontmatter
        .get("attachmentId")
        .and_then(|v| v.as_str())
        .map(String::from)
        .ok_or_else(|| anyhow::anyhow!("No attachmentId found in file note body"))
}

fn get_file_extension(filename: &str, content_type: &str) -> String {
    // First try to get extension from filename
    if let Some(pos) = filename.rfind('.') {
        return filename[pos..].to_string();
    }

    // Fallback to content type
    match content_type {
        "application/pdf" => ".pdf",
        "image/png" => ".png",
        "image/jpeg" | "image/jpg" => ".jpg",
        "image/gif" => ".gif",
        "image/svg+xml" => ".svg",
        "text/plain" => ".txt",
        "application/zip" => ".zip",
        _ => "",
    }
    .to_string()
}

#[allow(dead_code)]
pub fn get_file_note_paths(dir: &Path, slug: &str) -> Option<(PathBuf, PathBuf)> {
    let sidecar_path = dir.join(format!("{}.doqium", slug));

    if !sidecar_path.exists() {
        return None;
    }

    // Find the actual file (any non-.md, non-.doqium file with matching slug)
    if let Ok(entries) = fs::read_dir(dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            if !path.is_file() {
                continue;
            }

            if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
                if stem == slug {
                    if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
                        if ext != "md" && ext != "doqium" {
                            return Some((path, sidecar_path));
                        }
                    }
                }
            }
        }
    }

    None
}

pub fn build_note_request_from_frontmatter(
    frontmatter: &serde_json::Value,
    content: String,
    existing_note: Option<&Note>,
) -> Result<serde_json::Value> {
    let has_frontmatter = frontmatter.as_object().map_or(false, |obj| !obj.is_empty());

    let labels = if has_frontmatter {
        frontmatter
            .get("labels")
            .and_then(|v| v.as_array())
            .cloned()
            .unwrap_or_else(Vec::new)
    } else if let Some(note) = existing_note {
        note.metadata
            .labels
            .iter()
            .map(|s| serde_json::Value::String(s.clone()))
            .collect()
    } else {
        Vec::new()
    };

    let note_type = if has_frontmatter {
        frontmatter
            .get("type")
            .and_then(|v| v.as_str())
            .map(|s| s.to_string())
            .or_else(|| existing_note.map(|n| n.metadata.note_type.clone()))
            .unwrap_or_else(|| "text".to_string())
    } else if let Some(note) = existing_note {
        note.metadata.note_type.clone()
    } else {
        "text".to_string()
    };

    let title = if has_frontmatter {
        if let Some(fm_title) = frontmatter.get("title").and_then(|v| v.as_str()) {
            fm_title.to_string()
        } else {
            extract_title_from_content(&content)
        }
    } else {
        extract_title_from_content(&content)
    };

    let slug = if has_frontmatter {
        if let Some(fm_slug) = frontmatter.get("slug").and_then(|v| v.as_str()) {
            fm_slug.to_string()
        } else {
            sluggify(&title)
        }
    } else {
        sluggify(&title)
    };

    let favorite = frontmatter
        .get("favorite")
        .and_then(|v| v.as_bool())
        .or_else(|| existing_note.map(|n| n.metadata.favorite))
        .unwrap_or(false);

    let trashed = frontmatter
        .get("trashed")
        .and_then(|v| v.as_bool())
        .or_else(|| existing_note.map(|n| n.metadata.trashed))
        .unwrap_or(false);

    let archived = frontmatter
        .get("archived")
        .and_then(|v| v.as_bool())
        .or_else(|| existing_note.map(|n| n.metadata.archived))
        .unwrap_or(false);

    let is_public = frontmatter
        .get("public")
        .and_then(|p| p.as_bool())
        .or_else(|| existing_note.map(|n| n.metadata.public))
        .unwrap_or(false);

    let final_content = if !is_public {
        encrypt_note_content(&content)?
    } else {
        content
    };

    let mut metadata = serde_json::json!({
        "type": note_type,
        "title": title,
        "slug": slug,
        "labels": labels,
        "favorite": favorite,
        "trashed": trashed,
        "archived": archived,
        "public": is_public,
    });

    if let Some(note) = existing_note {
        metadata["id"] = serde_json::Value::String(note.metadata.id.clone());
    }

    Ok(serde_json::json!({
        "metadata": metadata,
        "content": final_content,
    }))
}

pub async fn update_note_property(
    token: &str,
    notebook_id: &str,
    note_id: &str,
    property: &str,
    value: serde_json::Value,
) -> Result<()> {
    let note = get_note(token, notebook_id, note_id).await?;

    let mut note_request = serde_json::json!({
        "metadata": {
            "id": note_id,
            "type": note.metadata.note_type,
            "title": note.metadata.title,
            "slug": note.metadata.slug,
            "labels": note.metadata.labels,
            "favorite": note.metadata.favorite,
            "trashed": note.metadata.trashed,
            "archived": note.metadata.archived,
            "public": note.metadata.public,
        },
        "content": note.content,
    });

    let is_becoming_private = property == "public" && value.as_bool() == Some(false);
    let is_becoming_public = property == "public" && value.as_bool() == Some(true);

    note_request["metadata"][property] = value;

    if is_becoming_private {
        if let Some(content) = note.content {
            let decrypted = if note.metadata.public {
                content
            } else {
                decrypt_note_content(&content)?
            };
            note_request["content"] = serde_json::Value::String(encrypt_note_content(&decrypted)?);
        }
    } else if is_becoming_public {
        if let Some(content) = note.content {
            let decrypted = if !note.metadata.public {
                decrypt_note_content(&content)?
            } else {
                content
            };
            note_request["content"] = serde_json::Value::String(decrypted);
        }
    }

    update_note(token, notebook_id, note_id, &note_request).await
}
