//! LastPass CSV importer.
//!
//! Pure: takes CSV bytes, returns a vector of `Item` (with freshly-minted
//! IDs and timestamps) plus a vector of `ImportWarning` for skipped or
//! partially-imported rows. Failed rows never abort the whole import;
//! the only fatal error is a missing or malformed header.
//!
//! Spec: docs/superpowers/specs/2026-04-27-relicario-import-export-design.md
//! (D10–D13 + the LastPass field-mapping table).

use serde::{Deserialize, Serialize};
use url::Url;
use zeroize::Zeroizing;

use crate::error::{RelicarioError, Result};
use crate::item::Item;
use crate::item_types::{ItemCore, LoginCore, SecureNoteCore};

/// LastPass column order. The header row must contain these exact column
/// names in this exact order.
pub const EXPECTED_HEADER: &[&str] =
    &["url", "username", "password", "totp", "extra", "name", "grouping", "fav"];

/// A row that was skipped, or partially imported with a downgrade
/// (e.g., login imported without TOTP).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImportWarning {
    /// 1-indexed row number in the CSV body (the header is row 0).
    pub row: usize,
    /// Title from the row's `name` column, if present and non-empty.
    pub title: Option<String>,
    /// Human-readable explanation, suitable for stderr / inline UI.
    pub message: String,
}

/// Parse a LastPass CSV export.
///
/// Returns the parsed items (with fresh IDs and timestamps) and any
/// per-row warnings. The function only fails if the header is missing
/// or doesn't match `EXPECTED_HEADER`.
pub fn parse_lastpass_csv(csv_bytes: &[u8]) -> Result<(Vec<Item>, Vec<ImportWarning>)> {
    let mut reader = csv::ReaderBuilder::new()
        .has_headers(true)
        .flexible(false)
        .from_reader(csv_bytes);

    // Validate header.
    let headers = reader
        .headers()
        .map_err(|e| RelicarioError::ImportCsvFormat(format!("read header: {e}")))?
        .clone();
    if headers.len() != EXPECTED_HEADER.len()
        || headers.iter().zip(EXPECTED_HEADER).any(|(got, want)| got != *want)
    {
        return Err(RelicarioError::ImportCsvHeader(format!(
            "expected `{}`, got `{}`",
            EXPECTED_HEADER.join(","),
            headers.iter().collect::<Vec<_>>().join(",")
        )));
    }

    let mut items = Vec::new();
    let mut warnings = Vec::new();

    for (idx, record) in reader.records().enumerate() {
        let row_num = idx + 1;
        let record = match record {
            Ok(r) => r,
            Err(e) => {
                warnings.push(ImportWarning {
                    row: row_num,
                    title: None,
                    message: format!("CSV parse error — skipped: {e}"),
                });
                continue;
            }
        };

        let (item, warn) = map_row(&record, row_num);
        if let Some(it) = item { items.push(it); }
        if let Some(w)  = warn  { warnings.push(w); }
    }

    Ok((items, warnings))
}

/// Map a single CSV record. Returns:
/// - `(Some(item), None)` for a fully-imported row.
/// - `(Some(item), Some(warn))` for a partially-imported row (e.g.,
///   bad TOTP base32 — login imported without TOTP).
/// - `(None, Some(warn))` for a skipped row (missing required field).
fn map_row(
    record: &csv::StringRecord,
    row: usize,
) -> (Option<Item>, Option<ImportWarning>) {
    let url      = record.get(0).unwrap_or("").trim();
    let username = record.get(1).unwrap_or("").trim();
    // password and extra are deliberately NOT trimmed: leading/trailing
    // whitespace is significant inside passwords and free-form notes.
    let password = record.get(2).unwrap_or("");
    let totp_raw = record.get(3).unwrap_or("").trim();
    let extra    = record.get(4).unwrap_or("");
    let name     = record.get(5).unwrap_or("").trim();
    let group    = record.get(6).unwrap_or("").trim();
    let fav      = record.get(7).unwrap_or("").trim();

    if name.is_empty() {
        return (None, Some(ImportWarning {
            row,
            title: None,
            message: "missing `name` — skipped".into(),
        }));
    }

    // SecureNote marker: LastPass exports notes with `url` set to "http://sn".
    // The `extra` column carries the body verbatim.
    if url == "http://sn" {
        let mut item = Item::new(
            name.to_string(),
            ItemCore::SecureNote(SecureNoteCore {
                body: Zeroizing::new(extra.to_string()),
            }),
        );
        item.group    = if group.is_empty() { None } else { Some(group.to_string()) };
        item.favorite = fav == "1";
        return (Some(item), None);
    }

    if password.is_empty() {
        return (None, Some(ImportWarning {
            row,
            title: Some(name.to_string()),
            message: "missing `password` — skipped".into(),
        }));
    }

    let mut warning: Option<ImportWarning> = None;

    let parsed_url = if url.is_empty() {
        None
    } else {
        match Url::parse(url) {
            Ok(u) => Some(u),
            Err(_) => {
                // Login still imports — URL becomes None, with a warning.
                if warning.is_none() {
                    warning = Some(ImportWarning {
                        row,
                        title: Some(name.to_string()),
                        message: format!("invalid URL `{url}` — login imported without URL"),
                    });
                }
                None
            }
        }
    };

    let totp = if totp_raw.is_empty() {
        None
    } else {
        match crate::base32::decode_rfc4648_lenient(totp_raw) {
            Ok(bytes) if !bytes.is_empty() => Some(crate::item_types::TotpConfig {
                secret: Zeroizing::new(bytes),
                algorithm: crate::item_types::TotpAlgorithm::Sha1,
                digits: 6,
                period_seconds: 30,
                kind: crate::item_types::TotpKind::Totp,
            }),
            _ => {
                if warning.is_none() {
                    warning = Some(ImportWarning {
                        row,
                        title: Some(name.to_string()),
                        message: "invalid base32 TOTP secret — login imported without TOTP"
                            .into(),
                    });
                }
                None
            }
        }
    };

    let mut item = Item::new(
        name.to_string(),
        ItemCore::Login(LoginCore {
            username: if username.is_empty() { None } else { Some(username.to_string()) },
            password: Some(Zeroizing::new(password.to_string())),
            url: parsed_url,
            totp,
        }),
    );
    item.group    = if group.is_empty() { None } else { Some(group.to_string()) };
    item.favorite = fav == "1";
    item.notes    = if extra.is_empty() { None } else { Some(extra.to_string()) };

    (Some(item), warning)
}

