use std::io::{self, IsTerminal, Write};

use comfy_table::{Cell, Color, ContentArrangement, Table, presets::UTF8_FULL};
use serde::Serialize;

use crate::errors::{CliError, ErrorKind};
use crate::types::{Classification, NameReport, Status, TakenInfo};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Format {
    Auto,
    Json,
    Table,
    Markdown,
}

impl Format {
    pub fn resolve(self) -> ResolvedFormat {
        match self {
            Self::Json => ResolvedFormat::Json,
            Self::Table => ResolvedFormat::Table {
                color: io::stdout().is_terminal(),
            },
            Self::Markdown => ResolvedFormat::Markdown,
            Self::Auto => {
                if io::stdout().is_terminal() {
                    ResolvedFormat::Table { color: true }
                } else {
                    ResolvedFormat::Json
                }
            }
        }
    }
}

#[derive(Debug, Clone, Copy)]
pub enum ResolvedFormat {
    Json,
    Table { color: bool },
    Markdown,
}

pub fn render(reports: &[NameReport], format: Format) -> Result<(), CliError> {
    let stdout = io::stdout();
    let mut w = stdout.lock();
    match format.resolve() {
        ResolvedFormat::Json => render_json(&mut w, reports),
        ResolvedFormat::Table { color } => render_table(&mut w, reports, color),
        ResolvedFormat::Markdown => render_markdown(&mut w, reports),
    }
}

fn render_json<W: Write>(w: &mut W, reports: &[NameReport]) -> Result<(), CliError> {
    let mut serializer = serde_json::Serializer::pretty(w);
    reports
        .serialize(&mut serializer)
        .map_err(|e| CliError::new(ErrorKind::Internal, format!("serialize: {e}")))?;
    let w = serializer.into_inner();
    writeln!(w).map_err(io_err)?;
    Ok(())
}

fn render_table<W: Write>(w: &mut W, reports: &[NameReport], color: bool) -> Result<(), CliError> {
    for (i, report) in reports.iter().enumerate() {
        if i > 0 {
            writeln!(w).map_err(io_err)?;
        }
        writeln!(w, "{}", report.name).map_err(io_err)?;
        let mut table = Table::new();
        table
            .load_preset(UTF8_FULL)
            .set_content_arrangement(ContentArrangement::Dynamic)
            .set_header(vec!["source", "status", "detail"]);
        for result in &report.results {
            let (label, classification) = status_label(&result.status);
            let detail = status_detail(&result.status);
            let status_cell = if color {
                Cell::new(label).fg(status_color(&result.status, classification))
            } else {
                Cell::new(label)
            };
            table.add_row(vec![
                Cell::new(&result.source),
                status_cell,
                Cell::new(detail),
            ]);
        }
        writeln!(w, "{table}").map_err(io_err)?;
        writeln!(w, "{}", summarize(report)).map_err(io_err)?;
    }
    Ok(())
}

fn render_markdown<W: Write>(w: &mut W, reports: &[NameReport]) -> Result<(), CliError> {
    for (i, report) in reports.iter().enumerate() {
        if i > 0 {
            writeln!(w).map_err(io_err)?;
        }
        writeln!(w, "## {}", report.name).map_err(io_err)?;
        writeln!(w).map_err(io_err)?;
        writeln!(w, "| source | status | detail |").map_err(io_err)?;
        writeln!(w, "| --- | --- | --- |").map_err(io_err)?;
        for result in &report.results {
            let (label, _) = status_label(&result.status);
            let detail = status_detail(&result.status).replace('|', "\\|");
            writeln!(w, "| {} | {} | {} |", result.source, label, detail).map_err(io_err)?;
        }
        writeln!(w).map_err(io_err)?;
        writeln!(w, "_{}_", summarize(report)).map_err(io_err)?;
    }
    Ok(())
}

fn status_label(status: &Status) -> (String, Option<Classification>) {
    match status {
        Status::Free => ("FREE".into(), None),
        Status::Taken(info) => (
            format!("TAKEN · {}", info.classification.label()),
            Some(info.classification),
        ),
        Status::Unknown { .. } => ("UNKNOWN".into(), None),
    }
}

fn status_color(status: &Status, classification: Option<Classification>) -> Color {
    match status {
        Status::Free => Color::Green,
        Status::Unknown { .. } => Color::DarkGrey,
        Status::Taken(_) => match classification.unwrap_or(Classification::Unknown) {
            Classification::Active => Color::Red,
            Classification::Placeholder => Color::Yellow,
            Classification::Dead => Color::DarkYellow,
            Classification::Unknown => Color::Magenta,
        },
    }
}

fn status_detail(status: &Status) -> String {
    match status {
        Status::Free => String::new(),
        Status::Taken(info) => describe(info),
        Status::Unknown { reason } => reason.clone(),
    }
}

fn describe(info: &TakenInfo) -> String {
    let mut parts: Vec<String> = Vec::new();
    if let Some(v) = &info.version {
        parts.push(format!("v{v}"));
    }
    if let Some(dl) = info.downloads {
        parts.push(format!("dl={dl}"));
    }
    if let Some(recent) = info.recent_downloads {
        parts.push(format!("recent={recent}"));
    }
    if let Some(last) = &info.last_release {
        parts.push(format!("released={}", &last[..last.len().min(10)]));
    }
    if let Some(owner) = &info.owner {
        parts.push(owner.clone());
    }
    if let Some(desc) = &info.description {
        parts.push(truncate(desc, 80));
    }
    parts.join(" · ")
}

fn truncate(s: &str, max: usize) -> String {
    if s.chars().count() <= max {
        return s.to_string();
    }
    let mut out: String = s.chars().take(max.saturating_sub(1)).collect();
    out.push('…');
    out
}

fn summarize(report: &NameReport) -> String {
    let total = report.results.len();
    let free = report
        .results
        .iter()
        .filter(|r| matches!(r.status, Status::Free))
        .count();
    let active = report
        .results
        .iter()
        .filter(|r| {
            matches!(
                &r.status,
                Status::Taken(info) if info.classification == Classification::Active
            )
        })
        .count();
    let verdict = match (free, active) {
        (f, _) if f == total => "CLEAN — available everywhere",
        (_, 0) => "WORKABLE — no active conflicts",
        _ => "CONTESTED — active project(s) hold this name",
    };
    format!("{free}/{total} free · {verdict}")
}

fn io_err(e: std::io::Error) -> CliError {
    CliError::new(ErrorKind::Internal, format!("write: {e}"))
}
