use base64::engine::general_purpose::STANDARD;
use base64::Engine;

use crate::email::Email;
use crate::error::Result;

/// Generate a MIME boundary string.
fn generate_boundary() -> String {
    let id = uuid::Uuid::new_v4();
    format!("----=_Part_{}", id.as_simple())
}

/// Build an RFC 5322 compliant email message from an Email struct.
pub fn build_rfc822(email: &Email) -> Result<String> {
    let mut output = String::with_capacity(4096);
    let message_id = email.message_id();

    // Required headers
    output.push_str(&format!(
        "From: {}\r\n",
        Email::sanitize_string(&email.from)
    ));

    if !email.to.is_empty() {
        output.push_str(&format!(
            "To: {}\r\n",
            email
                .to
                .iter()
                .map(|a| Email::sanitize_string(a))
                .collect::<Vec<_>>()
                .join(", ")
        ));
    }

    if !email.cc.is_empty() {
        output.push_str(&format!(
            "Cc: {}\r\n",
            email
                .cc
                .iter()
                .map(|a| Email::sanitize_string(a))
                .collect::<Vec<_>>()
                .join(", ")
        ));
    }

    output.push_str(&format!(
        "Subject: {}\r\n",
        Email::sanitize_string(&email.subject)
    ));
    output.push_str(&format!("Message-ID: {}\r\n", message_id));
    output.push_str(&format!("Date: {}\r\n", rfc2822_now()));
    output.push_str("MIME-Version: 1.0\r\n");

    // Priority headers
    match email.priority {
        crate::email::Priority::High => {
            output.push_str("X-Priority: 1\r\n");
            output.push_str("Importance: high\r\n");
        }
        crate::email::Priority::Low => {
            output.push_str("X-Priority: 5\r\n");
            output.push_str("Importance: low\r\n");
        }
        crate::email::Priority::Normal => {}
    }

    // Custom headers
    for (name, value) in &email.headers {
        output.push_str(&format!(
            "{}: {}\r\n",
            Email::sanitize_string(name),
            Email::sanitize_string(value)
        ));
    }

    let has_html = email.html.is_some();
    let has_attachments = !email.attachments.is_empty();

    match (has_html, has_attachments) {
        (false, false) => {
            // Plain text only
            output.push_str("Content-Type: text/plain; charset=UTF-8\r\n");
            output.push_str("Content-Transfer-Encoding: quoted-printable\r\n");
            output.push_str("\r\n");
            output.push_str(&quoted_printable_encode(&email.text));
        }
        (true, false) => {
            // multipart/alternative (text + html)
            let boundary = generate_boundary();
            output.push_str(&format!(
                "Content-Type: multipart/alternative; boundary=\"{}\"\r\n",
                boundary
            ));
            output.push_str("\r\n");

            // Text part
            output.push_str(&format!("--{}\r\n", boundary));
            output.push_str("Content-Type: text/plain; charset=UTF-8\r\n");
            output.push_str("Content-Transfer-Encoding: quoted-printable\r\n");
            output.push_str("\r\n");
            output.push_str(&quoted_printable_encode(&email.text));
            output.push_str("\r\n");

            // HTML part
            output.push_str(&format!("--{}\r\n", boundary));
            output.push_str("Content-Type: text/html; charset=UTF-8\r\n");
            output.push_str("Content-Transfer-Encoding: quoted-printable\r\n");
            output.push_str("\r\n");
            output.push_str(&quoted_printable_encode(email.html.as_deref().unwrap()));
            output.push_str("\r\n");

            output.push_str(&format!("--{}--\r\n", boundary));
        }
        (_, true) => {
            // multipart/mixed with optional multipart/alternative inside
            let mixed_boundary = generate_boundary();
            output.push_str(&format!(
                "Content-Type: multipart/mixed; boundary=\"{}\"\r\n",
                mixed_boundary
            ));
            output.push_str("\r\n");

            if has_html {
                // multipart/alternative for text+html
                let alt_boundary = generate_boundary();
                output.push_str(&format!("--{}\r\n", mixed_boundary));
                output.push_str(&format!(
                    "Content-Type: multipart/alternative; boundary=\"{}\"\r\n",
                    alt_boundary
                ));
                output.push_str("\r\n");

                // Text part
                output.push_str(&format!("--{}\r\n", alt_boundary));
                output.push_str("Content-Type: text/plain; charset=UTF-8\r\n");
                output.push_str("Content-Transfer-Encoding: quoted-printable\r\n");
                output.push_str("\r\n");
                output.push_str(&quoted_printable_encode(&email.text));
                output.push_str("\r\n");

                // HTML part
                output.push_str(&format!("--{}\r\n", alt_boundary));
                output.push_str("Content-Type: text/html; charset=UTF-8\r\n");
                output.push_str("Content-Transfer-Encoding: quoted-printable\r\n");
                output.push_str("\r\n");
                output.push_str(&quoted_printable_encode(email.html.as_deref().unwrap()));
                output.push_str("\r\n");

                output.push_str(&format!("--{}--\r\n", alt_boundary));
            } else {
                // Plain text only
                output.push_str(&format!("--{}\r\n", mixed_boundary));
                output.push_str("Content-Type: text/plain; charset=UTF-8\r\n");
                output.push_str("Content-Transfer-Encoding: quoted-printable\r\n");
                output.push_str("\r\n");
                output.push_str(&quoted_printable_encode(&email.text));
                output.push_str("\r\n");
            }

            // Attachments
            for attachment in &email.attachments {
                output.push_str(&format!("--{}\r\n", mixed_boundary));
                output.push_str(&format!(
                    "Content-Type: {}; name=\"{}\"\r\n",
                    attachment.content_type, attachment.filename
                ));
                output.push_str("Content-Transfer-Encoding: base64\r\n");

                if let Some(ref cid) = attachment.content_id {
                    output.push_str(&format!("Content-ID: <{}>\r\n", cid));
                    output.push_str("Content-Disposition: inline\r\n");
                } else {
                    output.push_str(&format!(
                        "Content-Disposition: attachment; filename=\"{}\"\r\n",
                        attachment.filename
                    ));
                }

                output.push_str("\r\n");
                output.push_str(&base64_encode_wrapped(&attachment.content));
                output.push_str("\r\n");
            }

            output.push_str(&format!("--{}--\r\n", mixed_boundary));
        }
    }

    Ok(output)
}

/// Encode a string as quoted-printable (RFC 2045).
fn quoted_printable_encode(input: &str) -> String {
    let mut output = String::with_capacity(input.len() * 2);
    let mut line_len = 0;

    for byte in input.bytes() {
        let encoded = match byte {
            // Printable ASCII that doesn't need encoding (except =)
            b' '..=b'<' | b'>'..=b'~' => {
                line_len += 1;
                (byte as char).to_string()
            }
            b'\t' => {
                line_len += 1;
                "\t".to_string()
            }
            b'\r' => continue, // handled with \n
            b'\n' => {
                line_len = 0;
                "\r\n".to_string()
            }
            _ => {
                line_len += 3;
                format!("={:02X}", byte)
            }
        };

        // Soft line break at 76 characters
        if line_len > 75 && byte != b'\n' {
            output.push_str("=\r\n");
            line_len = encoded.len();
        }

        output.push_str(&encoded);
    }

    output
}

/// Base64-encode binary data with 76-character line wrapping.
fn base64_encode_wrapped(data: &[u8]) -> String {
    let encoded = STANDARD.encode(data);
    let mut output = String::with_capacity(encoded.len() + encoded.len() / 76 * 2);
    for (i, ch) in encoded.chars().enumerate() {
        if i > 0 && i % 76 == 0 {
            output.push_str("\r\n");
        }
        output.push(ch);
    }
    output
}

/// Generate current date in RFC 2822 format (e.g., "Tue, 10 Feb 2026 12:00:00 +0000").
fn rfc2822_now() -> String {
    use std::time::SystemTime;

    let now = SystemTime::now()
        .duration_since(SystemTime::UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs();

    // Simple UTC formatting without chrono dependency
    let days = now / 86400;
    let time_of_day = now % 86400;
    let hours = time_of_day / 3600;
    let minutes = (time_of_day % 3600) / 60;
    let seconds = time_of_day % 60;

    // Calculate year/month/day from days since epoch
    let (year, month, day) = days_to_ymd(days);

    let day_of_week = ((days + 4) % 7) as usize; // Jan 1 1970 = Thursday (4)
    let dow = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
    let mon = [
        "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
    ];

    format!(
        "{}, {:02} {} {:04} {:02}:{:02}:{:02} +0000",
        dow[day_of_week],
        day,
        mon[(month - 1) as usize],
        year,
        hours,
        minutes,
        seconds
    )
}

/// Convert days since Unix epoch to (year, month, day).
fn days_to_ymd(days: u64) -> (u64, u64, u64) {
    // Algorithm from https://howardhinnant.github.io/date_algorithms.html
    let z = days + 719468;
    let era = z / 146097;
    let doe = z - era * 146097;
    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
    let y = yoe + era * 400;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    let mp = (5 * doy + 2) / 153;
    let d = doy - (153 * mp + 2) / 5 + 1;
    let m = if mp < 10 { mp + 3 } else { mp - 9 };
    let y = if m <= 2 { y + 1 } else { y };
    (y, m, d)
}

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

    #[test]
    fn test_plain_text_email() {
        let mut email = Email::new("sender@example.com", "Test Subject", "Hello World");
        email.add_to("recipient@example.com");
        email.set_message_id("<test@example.com>");

        let rfc822 = build_rfc822(&email).unwrap();
        assert!(rfc822.contains("From: sender@example.com"));
        assert!(rfc822.contains("To: recipient@example.com"));
        assert!(rfc822.contains("Subject: Test Subject"));
        assert!(rfc822.contains("Content-Type: text/plain; charset=UTF-8"));
        assert!(rfc822.contains("Hello World"));
    }

    #[test]
    fn test_html_email() {
        let mut email = Email::new("sender@example.com", "HTML Test", "Plain text");
        email.add_to("recipient@example.com");
        email.set_html("<p>HTML content</p>");
        email.set_message_id("<test@example.com>");

        let rfc822 = build_rfc822(&email).unwrap();
        assert!(rfc822.contains("multipart/alternative"));
        assert!(rfc822.contains("text/plain"));
        assert!(rfc822.contains("text/html"));
        assert!(rfc822.contains("Plain text"));
        assert!(rfc822.contains("HTML content"));
    }

    #[test]
    fn test_email_with_attachment() {
        let mut email = Email::new("sender@example.com", "Attachment Test", "See attached");
        email.add_to("recipient@example.com");
        email.set_message_id("<test@example.com>");
        email.add_attachment(crate::email::Attachment {
            filename: "test.txt".to_string(),
            content: b"Hello attachment".to_vec(),
            content_type: "text/plain".to_string(),
            content_id: None,
        });

        let rfc822 = build_rfc822(&email).unwrap();
        assert!(rfc822.contains("multipart/mixed"));
        assert!(rfc822.contains("Content-Disposition: attachment"));
        assert!(rfc822.contains("test.txt"));
    }

    #[test]
    fn test_quoted_printable() {
        let input = "Hello = World";
        let encoded = quoted_printable_encode(input);
        assert!(encoded.contains("=3D")); // = is encoded

        let input = "Plain ASCII text";
        let encoded = quoted_printable_encode(input);
        assert_eq!(encoded, "Plain ASCII text");
    }

    #[test]
    fn test_base64_wrapped() {
        let data = vec![0u8; 100];
        let encoded = base64_encode_wrapped(&data);
        for line in encoded.split("\r\n") {
            assert!(line.len() <= 76);
        }
    }

    #[test]
    fn test_rfc2822_date() {
        let date = rfc2822_now();
        // Should match pattern like "Tue, 10 Feb 2026 12:00:00 +0000"
        assert!(date.contains("+0000"));
        assert!(date.len() > 20);
    }
}
