use chrono::{DateTime, Utc};

use crate::{
    date::RECURRENCE_INTERVAL,
    filter,
    model::{Item, ViewDef},
};

pub(super) fn render_ical(items: &[Item]) -> String {
    let mut output = String::from(
        "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Rogue Agenda//EN\r\nCALSCALE:GREGORIAN\r\n",
    );
    for item in items {
        output.push_str("BEGIN:VTODO\r\n");
        output.push_str(&format!("UID:rogue-{}@local\r\n", item.id));
        output.push_str(&format!(
            "DTSTAMP:{}\r\n",
            ical_timestamp(&item.created_at)
                .unwrap_or_else(|| Utc::now().format("%Y%m%dT%H%M%SZ").to_string())
        ));
        output.push_str(&format!("SUMMARY:{}\r\n", ical_escape(&item.text)));
        if !item.note.is_empty() {
            output.push_str(&format!("DESCRIPTION:{}\r\n", ical_escape(&item.note)));
        }
        if let Some(when_at) = item.when_at.as_deref().and_then(ical_timestamp) {
            output.push_str(&format!("DUE:{when_at}\r\n"));
        }
        output.push_str(&format!("PRIORITY:{}\r\n", item.priority));
        if !item.categories.is_empty() {
            output.push_str(&format!(
                "CATEGORIES:{}\r\n",
                item.categories
                    .iter()
                    .map(|category| ical_escape(&category.name))
                    .collect::<Vec<_>>()
                    .join(",")
            ));
        }
        if let Some(rule) = recurrence_to_rrule(&item.recurrence) {
            output.push_str(&format!("RRULE:{rule}\r\n"));
        }
        if let Some(done) = item.done_at.as_deref() {
            output.push_str("STATUS:COMPLETED\r\n");
            if let Some(done) = ical_timestamp(done) {
                output.push_str(&format!("COMPLETED:{done}\r\n"));
            }
        } else {
            output.push_str("STATUS:NEEDS-ACTION\r\n");
        }
        output.push_str("END:VTODO\r\n");
    }
    output.push_str("END:VCALENDAR\r\n");
    output
}

fn ical_escape(value: &str) -> String {
    value
        .replace('\\', "\\\\")
        .replace('\n', "\\n")
        .replace(',', "\\,")
        .replace(';', "\\;")
}

fn ical_timestamp(value: &str) -> Option<String> {
    DateTime::parse_from_rfc3339(value).ok().map(|date_time| {
        date_time
            .with_timezone(&Utc)
            .format("%Y%m%dT%H%M%SZ")
            .to_string()
    })
}

fn recurrence_to_rrule(value: &str) -> Option<String> {
    let value = value.trim().to_lowercase();
    if value.is_empty() {
        return None;
    }
    Some(match value.as_str() {
        "daily" => "FREQ=DAILY".into(),
        "weekdays" => "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR".into(),
        "weekly" => "FREQ=WEEKLY".into(),
        "monthly" => "FREQ=MONTHLY".into(),
        "yearly" | "annually" => "FREQ=YEARLY".into(),
        _ => {
            let captures = RECURRENCE_INTERVAL.captures(&value)?;
            if captures[1].parse::<u32>().ok()? == 0 {
                return None;
            }
            let frequency = match &captures[2] {
                "day" | "days" => "DAILY",
                "week" | "weeks" => "WEEKLY",
                "month" | "months" => "MONTHLY",
                _ => "YEARLY",
            };
            format!("FREQ={frequency};INTERVAL={}", &captures[1])
        }
    })
}

pub(super) fn render_markdown(
    view: &ViewDef,
    items: &[Item],
    description: &str,
    show_headers: bool,
) -> String {
    let generated = Utc::now().to_rfc3339();
    let mut output = String::new();
    if show_headers {
        output.push_str(&format!("# Rogue Agenda — {}\n\n", view.name));
        if !description.is_empty() {
            output.push_str(&format!("{}\n\n", markdown(description)));
        }
        output.push_str(&format!(
            "View: **{}**  \nFilters: `{}`  \nGenerated: {}\n\n",
            markdown(&view.name),
            markdown(&filter_summary(view)),
            generated
        ));
    }
    for (heading, group) in report_groups(view, items) {
        if !heading.is_empty() {
            output.push_str(&format!("## {}\n\n", markdown(&heading)));
        }
        output.push('|');
        for column in report_columns(view) {
            output.push_str(&format!(" {} |", markdown(&column.heading())));
        }
        output.push('\n');
        output.push('|');
        for column in report_columns(view) {
            output.push_str(match column.alignment() {
                "right" => " ---: |",
                "center" => " :---: |",
                _ => " :--- |",
            });
        }
        output.push('\n');
        for item in &group {
            output.push('|');
            for column in report_columns(view) {
                output.push_str(&format!(" {} |", markdown(&column.value(item, &group))));
            }
            output.push('\n');
        }
        if view
            .columns
            .iter()
            .any(|column| column.aggregate != crate::model::Aggregate::None)
        {
            output.push('|');
            for column in report_columns(view) {
                output.push_str(&format!(" {} |", markdown(&column.aggregate(&group))));
            }
            output.push('\n');
        }
        output.push('\n');
    }
    output
}

pub(super) fn render_html(
    view: &ViewDef,
    items: &[Item],
    description: &str,
    show_headers: bool,
) -> String {
    let generated = Utc::now().to_rfc3339();
    let mut output = format!(
        "<!doctype html><html><head><meta charset=\"utf-8\"><title>Rogue Agenda — {}</title><style>body{{font:16px monospace;background:#c0c0c0;color:#000}}h1,h2{{background:#000080;color:white;padding:.35rem}}table{{border-collapse:collapse;width:100%;margin-bottom:1.5rem;break-inside:auto}}thead{{display:table-header-group}}tfoot{{display:table-footer-group;font-weight:bold}}tr{{break-inside:avoid}}th{{color:#000080}}th,td{{border-bottom:1px solid #888;padding:.3rem;vertical-align:top;overflow-wrap:anywhere}}@media print{{body{{background:white}}h1,h2{{color:black;background:white;border-bottom:2px solid black}}section{{break-inside:avoid-page}}}}</style></head><body>",
        html(&view.name),
    );
    if show_headers {
        output.push_str(&format!(
            "<header><h1>Rogue Agenda — {}</h1><p>{}</p><p>View: {} · Filters: <code>{}</code> · Generated: {}</p></header>",
            html(&view.name),
            html(description),
            html(&view.name),
            html(&filter_summary(view)),
            html(&generated),
        ));
    }
    for (heading, group) in report_groups(view, items) {
        output.push_str("<section>");
        if !heading.is_empty() {
            output.push_str(&format!("<h2>{}</h2>", html(&heading)));
        }
        output.push_str("<table><colgroup>");
        for column in report_columns(view) {
            output.push_str(&format!("<col style=\"width:{}%\">", column.width()));
        }
        output.push_str("</colgroup><thead><tr>");
        for column in report_columns(view) {
            output.push_str(&format!(
                "<th style=\"text-align:{}\">{}</th>",
                column.alignment(),
                html(&column.heading())
            ));
        }
        output.push_str("</tr></thead><tbody>");
        for item in &group {
            output.push_str("<tr>");
            for column in report_columns(view) {
                output.push_str(&format!(
                    "<td style=\"text-align:{}\">{}</td>",
                    column.alignment(),
                    html(&column.value(item, &group)).replace('\n', "<br>")
                ));
            }
            output.push_str("</tr>");
        }
        output.push_str("</tbody>");
        if view
            .columns
            .iter()
            .any(|column| column.aggregate != crate::model::Aggregate::None)
        {
            output.push_str("<tfoot><tr>");
            for column in report_columns(view) {
                output.push_str(&format!(
                    "<td style=\"text-align:{}\">{}</td>",
                    column.alignment(),
                    html(&column.aggregate(&group))
                ));
            }
            output.push_str("</tr></tfoot>");
        }
        output.push_str("</table></section>");
    }
    if show_headers {
        output.push_str(&format!(
            "<footer>Rogue Agenda · {} · {generated}</footer>",
            html(&view.name)
        ));
    }
    output.push_str("</body></html>\n");
    output
}

fn filter_summary(view: &ViewDef) -> String {
    format!(
        "kind={} value={} expression={} show_done={}",
        view.kind, view.filter_value, view.filter_expr, view.show_done
    )
}

fn report_groups<'a>(view: &ViewDef, items: &'a [Item]) -> Vec<(String, Vec<&'a Item>)> {
    if view.sections.is_empty() {
        return vec![(String::new(), items.iter().collect())];
    }
    view.sections
        .iter()
        .map(|section| {
            (
                section.heading.clone(),
                items
                    .iter()
                    .filter(|item| filter::matches(&section.filter_expr, item).unwrap_or(false))
                    .collect(),
            )
        })
        .collect()
}

enum ReportColumn<'a> {
    Value(&'a crate::model::ViewColumn),
    Percent(&'a crate::model::ViewColumn),
}

impl ReportColumn<'_> {
    fn heading(&self) -> String {
        match self {
            Self::Value(column) => column.heading.clone(),
            Self::Percent(column) => format!("{} %", column.heading),
        }
    }

    fn alignment(&self) -> &str {
        match self {
            Self::Value(column) => &column.alignment,
            Self::Percent(_) => "right",
        }
    }

    fn width(&self) -> u16 {
        match self {
            Self::Value(column) => column.width,
            Self::Percent(_) => 10,
        }
    }

    fn value(&self, item: &Item, section: &[&Item]) -> String {
        match self {
            Self::Value(column) => column
                .numeric_value(item)
                .map(|value| column.format_number(value))
                .unwrap_or_else(|| column.raw_value(item)),
            Self::Percent(column) => {
                let total = section
                    .iter()
                    .filter_map(|item| column.numeric_value(item))
                    .sum::<f64>();
                column
                    .numeric_value(item)
                    .filter(|_| total != 0.0)
                    .map(|value| format!("{:.2}%", value * 100.0 / total))
                    .unwrap_or_default()
            }
        }
    }

    fn aggregate(&self, section: &[&Item]) -> String {
        match self {
            Self::Value(column) => column.aggregate_value(section.iter().copied()),
            Self::Percent(_) => String::new(),
        }
    }
}

fn report_columns(view: &ViewDef) -> Vec<ReportColumn<'_>> {
    view.columns
        .iter()
        .flat_map(|column| {
            std::iter::once(ReportColumn::Value(column)).chain(
                column
                    .percent_total
                    .then_some(ReportColumn::Percent(column)),
            )
        })
        .collect()
}

fn markdown(value: &str) -> String {
    value.replace('|', "\\|").replace('\n', "<br>")
}

fn html(value: &str) -> String {
    value
        .replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
}

pub(super) fn json(value: &str) -> String {
    format!(
        "\"{}\"",
        value
            .replace('\\', "\\\\")
            .replace('"', "\\\"")
            .replace('\n', "\\n")
            .replace('\r', "\\r")
    )
}

pub(super) fn opt_json(value: Option<&str>) -> String {
    value.map(json).unwrap_or_else(|| "null".into())
}

pub(super) fn csv(value: &str) -> String {
    format!("\"{}\"", value.replace('"', "\"\""))
}
