mod audit;
mod graph;
mod read;
mod write;
use crate::prelude::*;
pub(crate) use audit::*;
pub(crate) use graph::*;
pub(crate) use read::*;
pub(crate) use write::*;

#[derive(Clone, Debug)]
pub(crate) struct ContactResolveOptions {
    pub(crate) payload: Map<String, Value>,
    pub(crate) candidate_limit: usize,
    pub(crate) one: bool,
    pub(crate) all: bool,
}

impl ContactResolveOptions {
    pub(crate) fn from_matches(matches: &ArgMatches) -> Result<Self> {
        let spec = search_command_spec();
        let mut payload = parse_payload(&spec, matches)?;
        payload.remove("limit");
        let all = matches.get_flag("all");
        if !all && !contacts_resolve_has_search_filter(&payload) {
            return err("contacts:resolve requires at least one search filter, or --all");
        }
        Ok(Self {
            payload,
            candidate_limit: optional_usize_from_matches(matches, "candidate-limit")?
                .unwrap_or(CONTACT_RESOLVE_CANDIDATE_LIMIT_DEFAULT),
            one: matches.get_flag("one"),
            all,
        })
    }
}

pub(crate) fn min_confidence_from_matches(matches: &ArgMatches) -> Result<u64> {
    let raw = matches
        .get_one::<String>("min-confidence")
        .map(String::as_str)
        .unwrap_or("60");
    let value = raw
        .parse::<u64>()
        .into_diagnostic()
        .wrap_err("--min-confidence must be an integer from 0 to 100")?;
    if value > 100 {
        return err("--min-confidence must be at most 100");
    }
    Ok(value)
}

pub(crate) fn live_search_count_dry_run_payload(payload: Option<&Map<String, Value>>) -> Value {
    let mut payload = payload.cloned().unwrap_or_default();
    payload.set("limit", 0_u64);
    Value::Object(payload)
}

pub(crate) fn live_search_page_dry_run_payload(payload: Option<&Map<String, Value>>) -> Value {
    let mut payload = payload.cloned().unwrap_or_default();
    payload.insert(
        "limit".to_string(),
        Value::Number(Number::from(SEARCH_LIMIT_MAX as u64)),
    );
    payload.insert(
        "exclude_contact_ids".to_string(),
        Value::String("accumulated from prior pages".to_string()),
    );
    Value::Object(payload)
}

pub(crate) fn remove_nested_source(report: &mut Value) {
    if let Some(object) = report.as_object_mut() {
        object.remove("source");
    }
}

pub(crate) fn object_values_by_aliases<'a>(
    object: &'a Map<String, Value>,
    aliases: &[&str],
) -> Vec<&'a Value> {
    object
        .iter()
        .filter(|(key, _)| aliases.iter().any(|alias| key_matches(key, &[*alias])))
        .map(|(_, value)| value)
        .collect()
}

pub(crate) fn object_string_by_aliases(
    object: &Map<String, Value>,
    aliases: &[&str],
) -> Option<String> {
    object_values_by_aliases(object, aliases)
        .into_iter()
        .filter_map(value_string)
        .map(|value| value.trim().to_string())
        .find(|value| !value.is_empty())
}

pub(crate) fn contact_set_definitions(
    options: &ContactSetsOptions,
) -> Result<Vec<ContactSegmentDefinition>> {
    contact_segment_definitions_from_input(&options.input, options.input_format, "contacts:sets")
}

pub(crate) fn row_value_by_aliases<'a>(
    row: &'a Map<String, Value>,
    aliases: &[String],
) -> Option<&'a Value> {
    row.iter()
        .find(|(key, _)| {
            aliases
                .iter()
                .any(|alias| key_matches(key, &[alias.as_str()]))
        })
        .map(|(_, value)| value)
}

pub(crate) fn contacts_for_input(
    path: &Path,
    requested_format: InputFormat,
    label: &'static str,
) -> Result<(Value, Vec<Value>)> {
    let text = fs::read_to_string(path)
        .into_diagnostic()
        .wrap_err_with(|| format!("reading {}", path.display()))?;
    let input_format = requested_format.resolve(path, &text);
    let rows = read_apply_rows(&text, input_format, label)?;
    let contacts = rows.into_iter().map(Value::Object).collect::<Vec<_>>();
    Ok((
        json!({
            "type": "input",
            "path": path.display().to_string(),
            "input_format": input_format.as_str(),
            "analyzed_count": contacts.len(),
        }),
        contacts,
    ))
}

pub(crate) fn contact_name(contact: &Value) -> Option<String> {
    first_contact_string(contact, &["name", "displayName", "display_name"])
}

pub(crate) fn first_contact_string(contact: &Value, keys: &[&str]) -> Option<String> {
    contact.as_object().and_then(|object| {
        keys.iter()
            .filter_map(|key| object.get(*key))
            .filter_map(value_string)
            .map(|value| value.trim().to_string())
            .find(|value| !value.is_empty())
    })
}

pub(crate) fn contact_string_values(contact: &Value, keys: &[&str]) -> Vec<String> {
    let Some(object) = contact.as_object() else {
        return Vec::new();
    };
    let mut values = Vec::new();
    for key in keys {
        if let Some(value) = object.get(*key) {
            collect_contact_strings(value, &mut values);
        }
    }
    dedupe_strings(values)
}

pub(crate) fn collect_contact_strings(value: &Value, values: &mut Vec<String>) {
    match value {
        Value::String(value) => {
            let value = value.trim();
            if !value.is_empty() {
                values.push(value.to_string());
            }
        }
        Value::Number(_) | Value::Bool(_) => {
            let value = cell_string(value);
            if !value.is_empty() {
                values.push(value);
            }
        }
        Value::Array(items) => {
            for item in items {
                collect_contact_strings(item, values);
            }
        }
        Value::Object(object) => {
            for key in ["value", "url", "email", "phone", "phone_number", "linkedin"] {
                if let Some(value) = object.get(key) {
                    collect_contact_strings(value, values);
                }
            }
        }
        Value::Null => {}
    }
}

pub(crate) fn normalize_email_key(value: &str) -> Option<String> {
    let value = value.trim();
    let candidate = value
        .rfind('<')
        .and_then(|start| {
            let inner = &value[start + 1..];
            inner.find('>').map(|end| inner[..end].trim())
        })
        .unwrap_or(value);
    let at = candidate.rfind('@')?;
    let local = candidate[..at]
        .trim_end_matches(is_email_local_separator)
        .rsplit(is_email_local_separator)
        .next()
        .unwrap_or_default()
        .trim_matches('\'');
    if local.is_empty() {
        return None;
    }
    let domain = email_domain_from_string(candidate)?;
    Some(format!("{}@{domain}", local.to_lowercase()))
}

/// Characters that cannot appear in an unquoted email local part, so display
/// strings like `Ada <ada@example.invalid>` or `mailto:ada@example.invalid`
/// split at them. RFC 6531 allows non-ASCII letters in internationalized
/// locals, so they must never be treated as separators.
fn is_email_local_separator(ch: char) -> bool {
    ch.is_whitespace()
        || matches!(
            ch,
            '<' | '>' | '(' | ')' | '[' | ']' | ':' | ';' | '@' | '\\' | ',' | '"'
        )
}

pub(crate) fn normalize_phone_key(value: &str) -> Option<String> {
    let digits = value
        .chars()
        .filter(|ch| ch.is_ascii_digit())
        .collect::<String>();
    (digits.len() >= 7).then_some(digits)
}

pub(crate) fn normalize_linkedin_key(value: &str) -> Option<String> {
    let mut value = value.trim().to_ascii_lowercase();
    if let Some(index) = value.find('#') {
        value.truncate(index);
    }
    if let Some(index) = value.find('?') {
        value.truncate(index);
    }
    for prefix in ["https://", "http://"] {
        if let Some(stripped) = value.strip_prefix(prefix) {
            value = stripped.to_string();
        }
    }
    if let Some(stripped) = value.strip_prefix("www.") {
        value = stripped.to_string();
    }
    while value.ends_with('/') {
        value.pop();
    }
    (value == "linkedin.com" || value.starts_with("linkedin.com/")).then_some(value)
}

pub(crate) fn normalize_name_key(value: &str) -> Option<String> {
    let mut normalized = String::new();
    for ch in value.chars().flat_map(char::to_lowercase) {
        if ch.is_alphanumeric() {
            normalized.push(ch);
        } else if ch.is_whitespace() && !normalized.ends_with(' ') {
            normalized.push(' ');
        }
    }
    let normalized = normalized.trim().to_string();
    (normalized.chars().count() >= 2).then_some(normalized)
}

pub(crate) fn first_object_string(object: &Map<String, Value>, keys: &[&str]) -> Option<String> {
    keys.iter()
        .filter_map(|key| object.get(*key).and_then(Value::as_str))
        .map(str::trim)
        .find(|value| !value.is_empty())
        .map(ToOwned::to_owned)
}

pub(crate) fn nested_name_string(value: Option<&Value>) -> Option<String> {
    match value {
        Some(Value::String(value)) => {
            let value = value.trim();
            (!value.is_empty()).then(|| value.to_string())
        }
        Some(Value::Object(object)) => first_object_string(object, &["name", "title"]),
        _ => None,
    }
}

pub(crate) fn insert_string_array(
    payload: &mut Map<String, Value>,
    key: &str,
    values: Vec<String>,
) {
    if !values.is_empty() {
        payload.insert(
            key.to_string(),
            Value::Array(values.into_iter().map(Value::String).collect()),
        );
    }
}

pub(crate) async fn write_bulk_contacts_json_array<W: Write>(
    runtime: &Runtime,
    ids: &[u64],
    concurrency: usize,
    pretty: bool,
    writer: &mut W,
) -> Result<()> {
    writer.write_all(b"[").into_diagnostic()?;
    let mut first = true;
    fetch_contacts_each(runtime, ids, concurrency, |_, row| {
        write_json_array_row(writer, &row, &mut first, pretty)
    })
    .await?;
    if pretty && !first {
        writer.write_all(b"\n").into_diagnostic()?;
    }
    writer.write_all(b"]\n").into_diagnostic()?;
    Ok(())
}

pub(crate) async fn write_json_array_contacts<W: Write>(
    runtime: &Runtime,
    payload: Map<String, Value>,
    page_size: usize,
    pretty: bool,
    writer: &mut W,
) -> Result<()> {
    writer.write_all(b"[").into_diagnostic()?;
    let mut first = true;
    export_all_contacts_each(runtime, payload, page_size, |row| {
        write_json_array_row(writer, &row, &mut first, pretty)
    })
    .await?;
    if pretty && !first {
        writer.write_all(b"\n").into_diagnostic()?;
    }
    writer.write_all(b"]\n").into_diagnostic()?;
    Ok(())
}

pub(crate) fn write_json_array_row<W: Write>(
    writer: &mut W,
    row: &Value,
    first: &mut bool,
    pretty: bool,
) -> Result<()> {
    if pretty {
        if *first {
            writer.write_all(b"\n").into_diagnostic()?;
        } else {
            writer.write_all(b",\n").into_diagnostic()?;
        }
        let text = serde_json::to_string_pretty(row)
            .into_diagnostic()
            .wrap_err("serializing JSON array row")?;
        for (index, line) in text.lines().enumerate() {
            if index != 0 {
                writer.write_all(b"\n").into_diagnostic()?;
            }
            writer.write_all(b"  ").into_diagnostic()?;
            writer.write_all(line.as_bytes()).into_diagnostic()?;
        }
    } else {
        if !*first {
            writer.write_all(b",").into_diagnostic()?;
        }
        serde_json::to_writer(&mut *writer, row)
            .into_diagnostic()
            .wrap_err("serializing compact JSON array row")?;
    }
    *first = false;
    Ok(())
}

pub(crate) fn collect_row_headers(row: &Value, headers: &mut BTreeSet<String>) -> Result<()> {
    let Value::Object(object) = row else {
        return err("me.sh search row was not an object");
    };
    headers.extend(object.keys().cloned());
    Ok(())
}

pub(crate) fn write_delimited_from_jsonl<W: Write>(
    spool_path: &Path,
    headers: &[String],
    delimiter: u8,
    writer: &mut W,
) -> Result<()> {
    if headers.is_empty() {
        return Ok(());
    }

    let file = fs::File::open(spool_path)
        .into_diagnostic()
        .wrap_err_with(|| format!("opening {}", spool_path.display()))?;
    let reader = StdBufReader::new(file);
    let mut csv_writer = csv::WriterBuilder::new()
        .delimiter(delimiter)
        .from_writer(writer);
    csv_writer.write_record(headers).into_diagnostic()?;
    for (index, line) in reader.lines().enumerate() {
        let line = line
            .into_diagnostic()
            .wrap_err_with(|| format!("reading {} line {}", spool_path.display(), index + 1))?;
        if line.trim().is_empty() {
            continue;
        }
        let value = serde_json::from_str::<Value>(&line)
            .into_diagnostic()
            .wrap_err_with(|| {
                format!(
                    "{} line {} is not valid JSON",
                    spool_path.display(),
                    index + 1
                )
            })?;
        let Value::Object(row) = value else {
            return err(format!(
                "{} line {} is not a JSON object",
                spool_path.display(),
                index + 1
            ));
        };
        let record = headers
            .iter()
            .map(|header| row.get(header).map(cell_string).unwrap_or_default())
            .collect::<Vec<_>>();
        csv_writer.write_record(record).into_diagnostic()?;
    }
    csv_writer.flush().into_diagnostic()
}

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

    #[test]
    fn normalize_email_key_lowercases_mailto_addresses() {
        assert_eq!(
            normalize_email_key("mailto:Ada@Example.INVALID"),
            Some("ada@example.invalid".to_string())
        );
    }

    #[test]
    fn normalize_email_key_extracts_address_from_display_text() {
        assert_eq!(
            normalize_email_key("Ada Lovelace <Ada@Example.INVALID>"),
            Some("ada@example.invalid".to_string())
        );
    }

    #[test]
    fn normalize_email_key_stops_before_trailing_text() {
        assert_eq!(
            normalize_email_key("ada@example.invalid phone"),
            Some("ada@example.invalid".to_string())
        );
    }

    #[test]
    fn normalize_email_key_keeps_unicode_locals_distinct() {
        assert_eq!(
            normalize_email_key("maría@example.invalid"),
            Some("maría@example.invalid".to_string())
        );
        assert_eq!(
            normalize_email_key("sofía@example.invalid"),
            Some("sofía@example.invalid".to_string())
        );
    }

    #[test]
    fn normalize_email_key_keeps_fully_non_ascii_locals() {
        assert_eq!(
            normalize_email_key("李明@example.invalid"),
            Some("李明@example.invalid".to_string())
        );
    }

    #[test]
    fn normalize_email_key_extracts_unicode_address_from_display_text() {
        assert_eq!(
            normalize_email_key("María García <maría@example.invalid>"),
            Some("maría@example.invalid".to_string())
        );
    }

    #[test]
    fn normalize_email_key_folds_unicode_uppercase_locals() {
        assert_eq!(
            normalize_email_key("MARÍA@Example.INVALID"),
            Some("maría@example.invalid".to_string())
        );
    }

    #[test]
    fn normalize_email_key_rejects_missing_or_invalid_addresses() {
        assert_eq!(normalize_email_key("not an email"), None);
        assert_eq!(normalize_email_key("@example.com"), None);
        assert_eq!(normalize_email_key("ada@example"), None);
        assert_eq!(normalize_email_key("ada@.com"), None);
        assert_eq!(normalize_email_key("ada@example."), None);
    }

    #[test]
    fn normalize_linkedin_key_normalizes_linkedin_urls() {
        assert_eq!(
            normalize_linkedin_key("https://www.linkedin.com/in/ada/?trk=profile"),
            Some("linkedin.com/in/ada".to_string())
        );
    }

    #[test]
    fn normalize_linkedin_key_rejects_non_linkedin_hosts() {
        assert_eq!(
            normalize_linkedin_key("https://notlinkedin.com/in/ada"),
            None
        );
        assert_eq!(
            normalize_linkedin_key("https://example.com/linkedin/ada"),
            None
        );
    }
}
