use std::collections::HashSet;

use anyhow::Result;
use axum::extract::{Path as AxumPath, Query, State};
use axum::response::Response;

use crate::db::Store;
use crate::model::{Effort, Priority, Status, Task, TaskId};
use crate::score;

use super::helpers::{error_response, friendly_date, friendly_status, list_projects_safe, render};
use super::AppState;

pub(super) mod mutations;
mod sorting;
pub(super) mod views;

use sorting::{SortField, SortOrder};
use views::{ProjectTemplate, ScoredEntry, SectionState, SortContext, TaskRow};

const PAGE_SIZE: usize = 25;

/// Query params for the project page. Each section has its own namespaced
/// set of filter/sort/page params (prefixed ip_, open_, closed_).
#[derive(serde::Deserialize, Default)]
pub(super) struct ProjectQuery {
    // Next-up scoring mode.
    next_mode: Option<String>,

    // In-progress section.
    ip_priority: Option<String>,
    ip_effort: Option<String>,
    ip_label: Option<String>,
    #[serde(rename = "ip_type")]
    ip_task_type: Option<String>,
    ip_q: Option<String>,
    ip_page: Option<usize>,
    ip_sort: Option<String>,
    ip_order: Option<String>,

    // Open section.
    open_priority: Option<String>,
    open_effort: Option<String>,
    open_label: Option<String>,
    #[serde(rename = "open_type")]
    open_task_type: Option<String>,
    open_q: Option<String>,
    open_page: Option<usize>,
    open_sort: Option<String>,
    open_order: Option<String>,

    // Closed section.
    closed_priority: Option<String>,
    closed_effort: Option<String>,
    closed_label: Option<String>,
    #[serde(rename = "closed_type")]
    closed_task_type: Option<String>,
    closed_q: Option<String>,
    closed_page: Option<usize>,
    closed_sort: Option<String>,
    closed_order: Option<String>,
}

/// Per-section query params extracted from the namespaced query string.
struct SectionQuery {
    priority: Option<String>,
    effort: Option<String>,
    label: Option<String>,
    task_type: Option<String>,
    q: Option<String>,
    page: Option<usize>,
    sort: Option<String>,
    order: Option<String>,
}

impl SectionQuery {
    /// Returns true if any param differs from defaults (used for the
    /// `<details open>` heuristic).
    fn has_user_params(&self) -> bool {
        self.priority.is_some()
            || self.effort.is_some()
            || self.label.is_some()
            || self.task_type.is_some()
            || self.q.as_deref().is_some_and(|s| !s.is_empty())
            || self.page.is_some_and(|p| p > 1)
            || self.sort.is_some()
            || self.order.is_some()
    }
}

/// Build the preserve_qs for a given section — the full query string for the
/// current page state, minus the given section's sort/order/page (those get
/// rebuilt by sort and pagination links). Filter params for the target section
/// are also excluded since the filter form will supply them.
fn build_preserve_qs(query: &ProjectQuery, exclude_prefix: &str) -> String {
    let mut parts = Vec::new();

    // Carry next_mode through all section links.
    if query.next_mode.as_deref() == Some("effort") {
        parts.push("next_mode=effort".to_string());
    }

    for (prefix, sq) in [
        ("ip_", extract_section(query, "ip_")),
        ("open_", extract_section(query, "open_")),
        ("closed_", extract_section(query, "closed_")),
    ] {
        if prefix == exclude_prefix {
            // This section's params are managed by its own form/sort/pagination.
            continue;
        }
        if let Some(ref p) = sq.priority {
            parts.push(format!("{prefix}priority={p}"));
        }
        if let Some(ref e) = sq.effort {
            parts.push(format!("{prefix}effort={e}"));
        }
        if let Some(ref l) = sq.label {
            parts.push(format!("{prefix}label={l}"));
        }
        if let Some(ref tt) = sq.task_type {
            parts.push(format!("{prefix}type={tt}"));
        }
        let search = sq.q.unwrap_or_default();
        if !search.is_empty() {
            parts.push(format!("{prefix}q={search}"));
        }
        let sort_field = sq
            .sort
            .as_deref()
            .and_then(SortField::parse)
            .unwrap_or(SortField::Priority);
        let sort_order = sq
            .order
            .as_deref()
            .and_then(SortOrder::parse)
            .unwrap_or_else(|| sort_field.default_order());
        if sort_field != SortField::Priority || sort_order != SortOrder::Asc {
            parts.push(format!(
                "{prefix}sort={}&{prefix}order={}",
                sort_field.as_str(),
                sort_order.as_str()
            ));
        }
        if let Some(page) = sq.page {
            if page > 1 {
                parts.push(format!("{prefix}page={page}"));
            }
        }
    }

    parts.join("&")
}

/// Extract a section's query params from the flat ProjectQuery.
fn extract_section(query: &ProjectQuery, prefix: &str) -> SectionQuery {
    match prefix {
        "ip_" => SectionQuery {
            priority: query.ip_priority.clone(),
            effort: query.ip_effort.clone(),
            label: query.ip_label.clone(),
            task_type: query.ip_task_type.clone(),
            q: query.ip_q.clone(),
            page: query.ip_page,
            sort: query.ip_sort.clone(),
            order: query.ip_order.clone(),
        },
        "open_" => SectionQuery {
            priority: query.open_priority.clone(),
            effort: query.open_effort.clone(),
            label: query.open_label.clone(),
            task_type: query.open_task_type.clone(),
            q: query.open_q.clone(),
            page: query.open_page,
            sort: query.open_sort.clone(),
            order: query.open_order.clone(),
        },
        "closed_" => SectionQuery {
            priority: query.closed_priority.clone(),
            effort: query.closed_effort.clone(),
            label: query.closed_label.clone(),
            task_type: query.closed_task_type.clone(),
            q: query.closed_q.clone(),
            page: query.closed_page,
            sort: query.closed_sort.clone(),
            order: query.closed_order.clone(),
        },
        _ => SectionQuery {
            priority: None,
            effort: None,
            label: None,
            task_type: None,
            q: None,
            page: None,
            sort: None,
            order: None,
        },
    }
}

/// Build a SectionState from tasks of a given status and a section's query
/// params.
fn build_section(
    all_tasks: &[Task],
    status: Status,
    label: &'static str,
    prefix: &str,
    sq: &SectionQuery,
    base_href: &str,
    preserve_qs: String,
) -> SectionState {
    // All tasks with this status (unfiltered count).
    let status_tasks: Vec<&Task> = all_tasks.iter().filter(|t| t.status == status).collect();
    let total_count = status_tasks.len();

    // Apply filters.
    let mut filtered: Vec<&Task> = status_tasks;

    if let Some(ref p) = sq.priority {
        if !p.is_empty() {
            if let Ok(parsed) = Priority::parse(p) {
                filtered.retain(|t| t.priority == parsed);
            }
        }
    }
    if let Some(ref e) = sq.effort {
        if !e.is_empty() {
            if let Ok(parsed) = Effort::parse(e) {
                filtered.retain(|t| t.effort == parsed);
            }
        }
    }
    if let Some(ref l) = sq.label {
        if !l.is_empty() {
            filtered.retain(|t| t.labels.iter().any(|x| x == l));
        }
    }
    if let Some(ref tt) = sq.task_type {
        if !tt.is_empty() {
            filtered.retain(|t| t.task_type == *tt);
        }
    }
    let search_term = sq.q.clone().unwrap_or_default();
    if !search_term.is_empty() {
        let q = search_term.to_ascii_lowercase();
        filtered.retain(|t| t.title.to_ascii_lowercase().contains(&q));
    }

    let filtered_count = filtered.len();

    // Sort.
    let sort_field = sq
        .sort
        .as_deref()
        .and_then(SortField::parse)
        .unwrap_or(SortField::Priority);
    let sort_order = sq
        .order
        .as_deref()
        .and_then(SortOrder::parse)
        .unwrap_or_else(|| sort_field.default_order());
    sorting::sort_tasks(&mut filtered, sort_field, sort_order);

    // Pagination.
    let total_pages = if filtered_count == 0 {
        1
    } else {
        filtered_count.div_ceil(PAGE_SIZE)
    };
    let page = sq.page.unwrap_or(1).clamp(1, total_pages);
    let start = (page - 1) * PAGE_SIZE;
    let end = (start + PAGE_SIZE).min(filtered_count);

    let tasks: Vec<TaskRow> = filtered[start..end]
        .iter()
        .map(|t| {
            let s = t.status.as_str().to_string();
            TaskRow {
                full_id: t.id.as_str().to_string(),
                short_id: t.id.short(),
                status_display: friendly_status(&s),
                status: s,
                task_type: t.task_type.clone(),
                priority: t.priority.as_str().to_string(),
                effort: t.effort.as_str().to_string(),
                title: t.title.clone(),
                labels: t.labels.clone(),
                created_at_display: friendly_date(&t.created_at),
                created_at: t.created_at.clone(),
            }
        })
        .collect();

    let sort_ctx = SortContext {
        base_href: base_href.to_string(),
        prefix: prefix.to_string(),
        field: sort_field.as_str().to_string(),
        order: sort_order.as_str().to_string(),
        preserve_qs,
    };

    SectionState {
        label,
        total_count,
        filtered_count,
        tasks,
        sort_ctx,
        filter_priority: sq.priority.clone(),
        filter_effort: sq.effort.clone(),
        filter_label: sq.label.clone(),
        filter_type: sq.task_type.clone(),
        filter_search: search_term,
        page,
        total_pages,
        pagination_pages: (1..=total_pages).collect(),
        has_user_params: sq.has_user_params(),
    }
}

pub(in crate::cmd::webui) async fn project_handler(
    State(state): State<AppState>,
    AxumPath(name): AxumPath<String>,
    Query(query): Query<ProjectQuery>,
) -> Response {
    let root = state.data_root.clone();
    let result = tokio::task::spawn_blocking(move || -> Result<ProjectTemplate> {
        let all_projects = list_projects_safe(&root);
        let store = Store::open(&root, &name)?;
        let tasks = store.list_tasks()?;

        // Stats from the full unfiltered set.
        let stats_open = tasks.iter().filter(|t| t.status == Status::Open).count();
        let stats_in_progress = tasks
            .iter()
            .filter(|t| t.status == Status::InProgress)
            .count();
        let stats_closed = tasks.iter().filter(|t| t.status == Status::Closed).count();

        // Collect distinct labels for the filter dropdown.
        let mut label_set: HashSet<String> = HashSet::new();
        for t in &tasks {
            for l in &t.labels {
                label_set.insert(l.clone());
            }
        }
        let mut all_labels: Vec<String> = label_set.into_iter().collect();
        all_labels.sort();

        // Next-up scoring (top 5 open tasks).
        let open_tasks: Vec<score::TaskInput> = tasks
            .iter()
            .filter(|t| t.status == Status::Open)
            .map(|t| score::TaskInput {
                id: t.id.as_str().to_string(),
                title: t.title.clone(),
                priority_score: t.priority.score(),
                effort_score: t.effort.score(),
                priority_label: t.priority.as_str().to_string(),
                effort_label: t.effort.as_str().to_string(),
            })
            .collect();

        let edges: Vec<(String, String)> = tasks
            .iter()
            .filter(|t| t.status == Status::Open)
            .flat_map(|t| {
                t.blockers
                    .iter()
                    .map(|b| (t.id.as_str().to_string(), b.as_str().to_string()))
                    .collect::<Vec<_>>()
            })
            .collect();

        let parents_with_open_children: HashSet<String> = tasks
            .iter()
            .filter(|t| t.status == Status::Open)
            .filter_map(|t| t.parent.as_ref().map(|p| p.as_str().to_string()))
            .collect();

        let next_mode = match query.next_mode.as_deref() {
            Some("effort") => score::Mode::Effort,
            _ => score::Mode::Impact,
        };
        let next_mode_str = match next_mode {
            score::Mode::Effort => "effort",
            score::Mode::Impact => "impact",
        };

        let scored = score::rank(
            &open_tasks,
            &edges,
            &parents_with_open_children,
            next_mode,
            5,
        );

        // Build a lookup from task ID to labels for the Next Up display.
        let labels_by_id: std::collections::HashMap<&str, &[String]> = tasks
            .iter()
            .map(|t| (t.id.as_str(), t.labels.as_slice()))
            .collect();

        let next_up: Vec<ScoredEntry> = scored
            .into_iter()
            .map(|s| {
                let equation = match next_mode {
                    score::Mode::Impact => format!(
                        "({:.2} + 1.00) × {:.2} / {:.2}^0.25 = {:.2}",
                        s.downstream_score, s.priority_weight, s.effort_weight, s.score
                    ),
                    score::Mode::Effort => format!(
                        "({:.2} × 0.25 + 1.00) × {:.2} / {:.2}² = {:.2}",
                        s.downstream_score, s.priority_weight, s.effort_weight, s.score
                    ),
                };
                let task_word = if s.total_unblocked == 1 {
                    "task"
                } else {
                    "tasks"
                };
                let unblocks_display = format!(
                    "Unblocks: {} {} ({} directly)",
                    s.total_unblocked, task_word, s.direct_unblocked
                );
                let labels = labels_by_id
                    .get(s.id.as_str())
                    .map(|ls| ls.to_vec())
                    .unwrap_or_default();
                ScoredEntry {
                    short_id: TaskId::display_id(&s.id),
                    id: s.id,
                    title: s.title,
                    score: format!("{:.2}", s.score),
                    status: "open".to_string(),
                    status_display: friendly_status("open"),
                    equation,
                    unblocks_display,
                    labels,
                }
            })
            .collect();

        let proj_name = store.project_name().to_string();
        let base_href = format!("/projects/{proj_name}");

        // Build each section with its own namespaced query params.
        let ip_sq = extract_section(&query, "ip_");
        let open_sq = extract_section(&query, "open_");
        let closed_sq = extract_section(&query, "closed_");

        let ip_preserve = build_preserve_qs(&query, "ip_");
        let open_preserve = build_preserve_qs(&query, "open_");
        let closed_preserve = build_preserve_qs(&query, "closed_");

        let in_progress = build_section(
            &tasks,
            Status::InProgress,
            "In progress",
            "ip_",
            &ip_sq,
            &base_href,
            ip_preserve,
        );
        let open = build_section(
            &tasks,
            Status::Open,
            "Open",
            "open_",
            &open_sq,
            &base_href,
            open_preserve,
        );
        let closed = build_section(
            &tasks,
            Status::Closed,
            "Closed",
            "closed_",
            &closed_sq,
            &base_href,
            closed_preserve,
        );

        Ok(ProjectTemplate {
            all_projects,
            active_project: Some(name),
            project_name: proj_name,
            stats_open,
            stats_in_progress,
            stats_closed,
            next_up,
            next_mode: next_mode_str.to_string(),
            all_labels,
            in_progress,
            open,
            closed,
        })
    })
    .await;

    match result {
        Ok(Ok(tmpl)) => render(tmpl),
        Ok(Err(e)) => error_response(500, &format!("{e}"), &[]),
        Err(e) => error_response(500, &format!("join error: {e}"), &[]),
    }
}
