use std::collections::HashSet;
use std::path::Path;
use std::sync::Arc;

use anyhow::Result;
use askama::Template;
use axum::extract::{Path as AxumPath, Query, State};
use axum::http::StatusCode;
use axum::response::{Html, IntoResponse, Response};
use axum::routing::get;
use axum::Router;

use crate::db::{self, Store, TaskId};
use crate::score;

const PAGE_SIZE: usize = 25;

// ---------------------------------------------------------------------------
// Shared state
// ---------------------------------------------------------------------------

#[derive(Clone)]
struct AppState {
    data_root: Arc<std::path::PathBuf>,
}

// ---------------------------------------------------------------------------
// Template view-models
// ---------------------------------------------------------------------------

/// A project card on the root page — either healthy or failed.
enum ProjectCard {
    Ok {
        name: String,
        open: usize,
        in_progress: usize,
        closed: usize,
        total: usize,
    },
    Err {
        name: String,
        error: String,
    },
}

/// Minimal view-model for a scored task in the "Next Up" table.
struct ScoredEntry {
    id: String,
    short_id: String,
    title: String,
    score: String,
}

/// Minimal view-model for a task row in the project task table.
struct TaskRow {
    full_id: String,
    short_id: String,
    status: String,
    priority: String,
    effort: String,
    title: String,
}

/// View-model for the task detail page.
struct TaskView {
    full_id: String,
    short_id: String,
    title: String,
    description: String,
    task_type: String,
    status: String,
    priority: String,
    effort: String,
    created_at: String,
    updated_at: String,
    labels: Vec<String>,
    logs: Vec<LogView>,
}

struct LogView {
    timestamp: String,
    message: String,
}

/// A blocker reference for the task detail page.
struct BlockerRef {
    full_id: String,
    short_id: String,
}

// ---------------------------------------------------------------------------
// Askama templates
// ---------------------------------------------------------------------------

#[derive(Template)]
#[template(path = "index.html")]
struct IndexTemplate {
    all_projects: Vec<String>,
    active_project: Option<String>,
    projects: Vec<ProjectCard>,
}

#[derive(Template)]
#[template(path = "project.html")]
struct ProjectTemplate {
    all_projects: Vec<String>,
    active_project: Option<String>,
    project_name: String,
    stats_open: usize,
    stats_in_progress: usize,
    stats_closed: usize,
    next_up: Vec<ScoredEntry>,
    page_tasks: Vec<TaskRow>,
    all_labels: Vec<String>,
    filter_status: Option<String>,
    filter_priority: Option<String>,
    filter_effort: Option<String>,
    filter_label: Option<String>,
    filter_search: String,
    page: usize,
    total_pages: usize,
    pagination_pages: Vec<usize>,
}

impl ProjectTemplate {
    /// Build a pagination link preserving current filter query params.
    fn pagination_href(&self, target_page: &usize) -> String {
        let target_page = *target_page;
        let mut parts = Vec::new();
        if let Some(ref s) = self.filter_status {
            parts.push(format!("status={s}"));
        }
        if let Some(ref p) = self.filter_priority {
            parts.push(format!("priority={p}"));
        }
        if let Some(ref e) = self.filter_effort {
            parts.push(format!("effort={e}"));
        }
        if let Some(ref l) = self.filter_label {
            parts.push(format!("label={l}"));
        }
        if !self.filter_search.is_empty() {
            parts.push(format!("q={}", self.filter_search));
        }
        parts.push(format!("page={target_page}"));
        format!("/projects/{}?{}", self.project_name, parts.join("&"))
    }
}

#[derive(Template)]
#[template(path = "task.html")]
struct TaskTemplate {
    all_projects: Vec<String>,
    active_project: Option<String>,
    project_name: String,
    task: TaskView,
    blockers_open: Vec<BlockerRef>,
    blockers_resolved: Vec<BlockerRef>,
    subtasks: Vec<TaskRow>,
}

#[derive(Template)]
#[template(path = "error.html")]
struct ErrorTemplate {
    all_projects: Vec<String>,
    active_project: Option<String>,
    status_code: u16,
    message: String,
}

// ---------------------------------------------------------------------------
// Response helpers
// ---------------------------------------------------------------------------

fn render(tmpl: impl Template) -> Response {
    match tmpl.render() {
        Ok(html) => Html(html).into_response(),
        Err(e) => error_response(500, &format!("template render failed: {e}"), &[]),
    }
}

fn error_response(code: u16, msg: &str, all_projects: &[String]) -> Response {
    let body = ErrorTemplate {
        all_projects: all_projects.to_vec(),
        active_project: None,
        status_code: code,
        message: msg.to_string(),
    };
    let html = body
        .render()
        .unwrap_or_else(|_| format!("<h1>{code}</h1><p>{msg}</p>"));
    let status = StatusCode::from_u16(code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
    (status, Html(html)).into_response()
}

fn list_projects_safe(root: &std::path::Path) -> Vec<String> {
    db::list_projects_in(root).unwrap_or_default()
}

// ---------------------------------------------------------------------------
// Route handlers
// ---------------------------------------------------------------------------

async fn index_handler(State(state): State<AppState>) -> Response {
    let root = state.data_root.clone();
    let result = tokio::task::spawn_blocking(move || -> Result<IndexTemplate> {
        let projects = list_projects_safe(&root);
        let mut cards = Vec::with_capacity(projects.len());

        for name in &projects {
            match Store::open(&root, name) {
                Ok(store) => {
                    let tasks = store.list_tasks()?;
                    let open = tasks
                        .iter()
                        .filter(|t| t.status == db::Status::Open)
                        .count();
                    let in_progress = tasks
                        .iter()
                        .filter(|t| t.status == db::Status::InProgress)
                        .count();
                    let closed = tasks
                        .iter()
                        .filter(|t| t.status == db::Status::Closed)
                        .count();
                    cards.push(ProjectCard::Ok {
                        name: name.clone(),
                        open,
                        in_progress,
                        closed,
                        total: tasks.len(),
                    });
                }
                Err(e) => {
                    cards.push(ProjectCard::Err {
                        name: name.clone(),
                        error: format!("{e}"),
                    });
                }
            }
        }

        Ok(IndexTemplate {
            all_projects: projects,
            active_project: None,
            projects: cards,
        })
    })
    .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}"), &[]),
    }
}

#[derive(serde::Deserialize)]
struct ProjectQuery {
    status: Option<String>,
    priority: Option<String>,
    effort: Option<String>,
    label: Option<String>,
    q: Option<String>,
    page: Option<usize>,
}

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 == db::Status::Open)
            .count();
        let stats_in_progress = tasks
            .iter()
            .filter(|t| t.status == db::Status::InProgress)
            .count();
        let stats_closed = tasks
            .iter()
            .filter(|t| t.status == db::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 == db::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: db::priority_label(t.priority).to_string(),
                effort_label: db::effort_label(t.effort).to_string(),
            })
            .collect();

        let edges: Vec<(String, String)> = tasks
            .iter()
            .filter(|t| t.status == db::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 == db::Status::Open)
            .filter_map(|t| t.parent.as_ref().map(|p| p.as_str().to_string()))
            .collect();

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

        let next_up: Vec<ScoredEntry> = scored
            .into_iter()
            .map(|s| ScoredEntry {
                short_id: TaskId::display_id(&s.id),
                id: s.id,
                title: s.title,
                score: format!("{:.2}", s.score),
            })
            .collect();

        // Apply filters.
        let mut filtered: Vec<&db::Task> = tasks.iter().collect();

        if let Some(ref s) = query.status {
            if !s.is_empty() {
                if let Ok(parsed) = db::parse_status(s) {
                    filtered.retain(|t| t.status == parsed);
                }
            }
        }
        if let Some(ref p) = query.priority {
            if !p.is_empty() {
                if let Ok(parsed) = db::parse_priority(p) {
                    filtered.retain(|t| t.priority == parsed);
                }
            }
        }
        if let Some(ref e) = query.effort {
            if !e.is_empty() {
                if let Ok(parsed) = db::parse_effort(e) {
                    filtered.retain(|t| t.effort == parsed);
                }
            }
        }
        if let Some(ref l) = query.label {
            if !l.is_empty() {
                filtered.retain(|t| t.labels.iter().any(|x| x == l));
            }
        }
        let search_term = query.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));
        }

        // Sort: priority score ascending, then created_at.
        filtered.sort_by_key(|t| (t.priority.score(), t.created_at.clone()));

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

        let page_tasks: Vec<TaskRow> = filtered[start..end]
            .iter()
            .map(|t| TaskRow {
                full_id: t.id.as_str().to_string(),
                short_id: t.id.short(),
                status: db::status_label(t.status).to_string(),
                priority: db::priority_label(t.priority).to_string(),
                effort: db::effort_label(t.effort).to_string(),
                title: t.title.clone(),
            })
            .collect();

        Ok(ProjectTemplate {
            all_projects,
            active_project: Some(name),
            project_name: store.project_name().to_string(),
            stats_open,
            stats_in_progress,
            stats_closed,
            next_up,
            page_tasks,
            all_labels,
            filter_status: query.status,
            filter_priority: query.priority,
            filter_effort: query.effort,
            filter_label: query.label,
            filter_search: search_term,
            page,
            total_pages,
            pagination_pages: (1..=total_pages).collect(),
        })
    })
    .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}"), &[]),
    }
}

async fn task_handler(
    State(state): State<AppState>,
    AxumPath((name, id)): AxumPath<(String, String)>,
) -> Response {
    let root = state.data_root.clone();
    let result = tokio::task::spawn_blocking(move || -> Result<TaskTemplate> {
        let all_projects = list_projects_safe(&root);
        let store = Store::open(&root, &name)?;

        let task_id = db::resolve_task_id(&store, &id, false)?;
        let task = store
            .get_task(&task_id, false)?
            .ok_or_else(|| anyhow::anyhow!("task '{id}' not found"))?;

        // Partition blockers.
        let partition = db::partition_blockers(&store, &task.blockers)?;
        let blockers_open: Vec<BlockerRef> = partition
            .open
            .iter()
            .map(|b| BlockerRef {
                full_id: b.as_str().to_string(),
                short_id: b.short(),
            })
            .collect();
        let blockers_resolved: Vec<BlockerRef> = partition
            .resolved
            .iter()
            .map(|b| BlockerRef {
                full_id: b.as_str().to_string(),
                short_id: b.short(),
            })
            .collect();

        // Find subtasks.
        let all_tasks = store.list_tasks()?;
        let subtasks: Vec<TaskRow> = all_tasks
            .iter()
            .filter(|t| t.parent.as_ref() == Some(&task_id))
            .map(|t| TaskRow {
                full_id: t.id.as_str().to_string(),
                short_id: t.id.short(),
                status: db::status_label(t.status).to_string(),
                priority: db::priority_label(t.priority).to_string(),
                effort: db::effort_label(t.effort).to_string(),
                title: t.title.clone(),
            })
            .collect();

        let task_view = TaskView {
            full_id: task.id.as_str().to_string(),
            short_id: task.id.short(),
            title: task.title.clone(),
            description: task.description.clone(),
            task_type: task.task_type.clone(),
            status: db::status_label(task.status).to_string(),
            priority: db::priority_label(task.priority).to_string(),
            effort: db::effort_label(task.effort).to_string(),
            created_at: task.created_at.clone(),
            updated_at: task.updated_at.clone(),
            labels: task.labels.clone(),
            logs: task
                .logs
                .iter()
                .map(|l| LogView {
                    timestamp: l.timestamp.clone(),
                    message: l.message.clone(),
                })
                .collect(),
        };

        Ok(TaskTemplate {
            all_projects,
            active_project: Some(name),
            project_name: store.project_name().to_string(),
            task: task_view,
            blockers_open,
            blockers_resolved,
            subtasks,
        })
    })
    .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}"), &[]),
    }
}

async fn static_oat_css() -> impl IntoResponse {
    (
        [(axum::http::header::CONTENT_TYPE, "text/css; charset=utf-8")],
        include_bytes!("../../static/oat.min.css").as_slice(),
    )
}

async fn static_td_css() -> impl IntoResponse {
    (
        [(axum::http::header::CONTENT_TYPE, "text/css; charset=utf-8")],
        include_bytes!("../../static/td.css").as_slice(),
    )
}

async fn static_js() -> impl IntoResponse {
    (
        [(
            axum::http::header::CONTENT_TYPE,
            "application/javascript; charset=utf-8",
        )],
        include_bytes!("../../static/oat.min.js").as_slice(),
    )
}

// ---------------------------------------------------------------------------
// Entry point
// ---------------------------------------------------------------------------

pub fn run(cwd: &Path, host: &str, port: u16, explicit_project: Option<&str>) -> Result<()> {
    let data_root = db::data_root()?;
    let state = AppState {
        data_root: Arc::new(data_root),
    };

    let app = Router::new()
        .route("/", get(index_handler))
        .route("/projects/{name}", get(project_handler))
        .route("/projects/{name}/tasks/{id}", get(task_handler))
        .route("/static/oat.min.css", get(static_oat_css))
        .route("/static/td.css", get(static_td_css))
        .route("/static/oat.min.js", get(static_js))
        .with_state(state);

    let addr = format!("{host}:{port}");
    let root_url = format!("http://{addr}");

    // Resolve current project for a convenience URL.
    let project_url = match explicit_project {
        Some(p) => Some(format!("{root_url}/projects/{p}")),
        None => db::try_open(cwd)
            .ok()
            .flatten()
            .map(|s| format!("{root_url}/projects/{}", s.project_name())),
    };

    eprintln!("listening on {root_url}");
    if let Some(ref url) = project_url {
        eprintln!("project: {url}");
    }

    tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()?
        .block_on(async {
            let listener = tokio::net::TcpListener::bind(&addr).await?;
            axum::serve(listener, app).await?;
            Ok(())
        })
}
