//! plan 规范化 —— 单屏路径。
//!
//! 行为忠实 TS,但**一次性算分类、干净派生**,不做 TS 那种
//! in-place strip-then-reclassify(`orchestrator.ts:838-845` 自标
//! fragile)。

use crate::dashboard_columns::{
    infer_dashboard_section_height, infer_dashboard_section_width, is_dashboard_like_prompt,
};
use crate::plan::OrchestratorPlan;
use crate::types::DesignRequest;

/// 规范化产出的派生信息。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NormInfo {
    /// 根 frame 窄到移动端宽度 —— scaffold 阶段据此注入固定状态栏。
    pub is_mobile: bool,
}

/// 移动端宽度上限(含)—— ≤ 此值视为移动端单屏。
const MOBILE_MAX_WIDTH: f64 = 480.0;

/// subtask 的 id / label 命中即视为"状态栏"区块 —— 移动端由
/// scaffold 注入固定状态栏,plan 里若带状态栏 subtask 则剔除。
fn is_status_bar_subtask(id: &str, label: &str) -> bool {
    let hay = format!("{} {}", id.to_lowercase(), label.to_lowercase());
    hay.contains("status bar") || hay.contains("status-bar") || hay.contains("statusbar")
}

/// 就地规范化 `plan`:
/// - 一次性判定 `is_mobile`(根 frame 宽度);
/// - 移动端剔除 plan 自带的状态栏 subtask(状态栏改由 scaffold 注入);
/// - 给每个 subtask 赋 `id_prefix = id`、`parent_frame_id = 根 id`;
/// - dashboard-like plan:每个 subtask 的 region 用推断宽高覆写
///   (宽度无条件覆写;高度在 [inferred*0.6, inferred*1.6] 窗口内保留
///   LLM 值,超出则取推断值 —— 忠实 TS `normalizeOrchestratorPlan`
///   `orchestrator.ts:259-272`)。
pub fn normalize(plan: &mut OrchestratorPlan, req: &DesignRequest) -> NormInfo {
    let is_mobile = plan.root_frame.width <= MOBILE_MAX_WIDTH;

    if is_mobile {
        plan.subtasks
            .retain(|st| !is_status_bar_subtask(&st.id, &st.label));
    }

    let root_width = plan.root_frame.width;
    let dashboard_like = is_dashboard_like_prompt(&req.prompt, plan);

    let root_id = plan.root_frame.id.clone();
    for st in &mut plan.subtasks {
        st.id_prefix = st.id.clone();
        st.parent_frame_id = Some(root_id.clone());

        if dashboard_like {
            let inferred_width = infer_dashboard_section_width(st, root_width);
            let inferred_height = infer_dashboard_section_height(st);

            st.region.width = inferred_width;

            if st.region.height <= 0.0 {
                st.region.height = inferred_height;
            } else {
                let min_height = (inferred_height * 0.6).round();
                let max_height = (inferred_height * 1.6).round();
                st.region.height = f64::max(min_height, f64::min(st.region.height, max_height));
            }
        }
    }

    NormInfo { is_mobile }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::plan::{OrchestratorPlan, Region, RootFrameSpec, Subtask};

    fn req() -> DesignRequest {
        DesignRequest {
            prompt: "x".into(),
            model: None,
            provider: None,
            design_md: None,
            concurrency: 1,
            append_context: None,
            validation_enabled: true,

            visual_ref_enabled: false,
        }
    }

    fn subtask(id: &str, label: &str) -> Subtask {
        Subtask {
            id: id.into(),
            label: label.into(),
            region: Region {
                width: 100.0,
                height: 100.0,
            },
            id_prefix: String::new(),
            parent_frame_id: None,
            elements: None,
            screen: None,
            generated_root_id: None,
            existing_section_labels: None,
        }
    }

    fn plan(width: f64, subtasks: Vec<Subtask>) -> OrchestratorPlan {
        OrchestratorPlan {
            root_frame: RootFrameSpec {
                id: "root".into(),
                name: "P".into(),
                width,
                height: 800.0,
                layout: None,
                gap: None,
                padding: None,
                fill: None,
            },
            subtasks,
            style_guide_name: None,
        }
    }

    #[test]
    fn normalize_assigns_id_prefix_and_parent() {
        let mut p = plan(
            1200.0,
            vec![subtask("hero", "Hero"), subtask("feat", "Features")],
        );
        let info = normalize(&mut p, &req());
        assert!(!info.is_mobile);
        for st in &p.subtasks {
            assert_eq!(st.id_prefix, st.id);
            assert_eq!(st.parent_frame_id.as_deref(), Some("root"));
        }
    }

    #[test]
    fn normalize_flags_mobile_by_width() {
        let mut p = plan(390.0, vec![subtask("hero", "Hero")]);
        let info = normalize(&mut p, &req());
        assert!(info.is_mobile);
    }

    #[test]
    fn normalize_strips_status_bar_subtask_on_mobile() {
        let mut p = plan(
            390.0,
            vec![subtask("status-bar", "Status Bar"), subtask("hero", "Hero")],
        );
        normalize(&mut p, &req());
        assert_eq!(p.subtasks.len(), 1);
        assert_eq!(p.subtasks[0].id, "hero");
    }

    #[test]
    fn normalize_keeps_status_bar_subtask_on_desktop() {
        // 桌面端不剔除(只有移动端 scaffold 注入固定状态栏)。
        let mut p = plan(
            1200.0,
            vec![subtask("status-bar", "Status Bar"), subtask("hero", "Hero")],
        );
        normalize(&mut p, &req());
        assert_eq!(p.subtasks.len(), 2);
    }

    // -----------------------------------------------------------------------
    // Task C1 — dashboard branch
    // -----------------------------------------------------------------------

    fn dash_req(prompt: &str) -> DesignRequest {
        DesignRequest {
            prompt: prompt.into(),
            model: None,
            provider: None,
            design_md: None,
            concurrency: 1,
            append_context: None,
            validation_enabled: true,

            visual_ref_enabled: false,
        }
    }

    /// Build a dashboard-like plan with a sidebar + two data subtasks.
    fn dashboard_plan(root_width: f64) -> OrchestratorPlan {
        let st_sidebar = Subtask {
            id: "sidebar".into(),
            label: "Sidebar Navigation".into(),
            region: Region {
                width: 100.0,
                height: 500.0,
            },
            id_prefix: String::new(),
            parent_frame_id: None,
            elements: None,
            screen: None,
            generated_root_id: None,
            existing_section_labels: None,
        };
        // chart subtask — LLM-provided height 300, within [inferred*0.6, inferred*1.6]
        // inferred for "chart" = 320  →  min=192, max=512  →  300 in range → keep 300
        let st_chart = Subtask {
            id: "revenue-chart".into(),
            label: "Revenue Chart".into(),
            region: Region {
                width: 800.0,
                height: 300.0,
            },
            id_prefix: String::new(),
            parent_frame_id: None,
            elements: None,
            screen: None,
            generated_root_id: None,
            existing_section_labels: None,
        };
        // metric subtask — LLM-provided height 0 (invalid) → use inferred = 160
        let st_metric = Subtask {
            id: "kpi-metrics".into(),
            label: "KPI Metrics".into(),
            region: Region {
                width: 800.0,
                height: 0.0,
            },
            id_prefix: String::new(),
            parent_frame_id: None,
            elements: None,
            screen: None,
            generated_root_id: None,
            existing_section_labels: None,
        };
        OrchestratorPlan {
            root_frame: RootFrameSpec {
                id: "root".into(),
                name: "Dashboard".into(),
                width: root_width,
                height: 800.0,
                layout: None,
                gap: None,
                padding: None,
                fill: None,
            },
            subtasks: vec![st_sidebar, st_chart, st_metric],
            style_guide_name: None,
        }
    }

    #[test]
    fn normalize_dashboard_rewrites_width_per_infer() {
        // root_width = 1200  →  main_width = max(320, 1200-260) = 940
        // sidebar → 260
        // chart/revenue → round(940 * 0.62) = round(582.8) = 583
        // metric (default) → 940
        let mut p = dashboard_plan(1200.0);
        let req = dash_req("an analytics admin dashboard");
        normalize(&mut p, &req);

        let sidebar = p.subtasks.iter().find(|s| s.id == "sidebar").unwrap();
        assert_eq!(sidebar.region.width, 260.0, "sidebar width must be 260");

        let chart = p.subtasks.iter().find(|s| s.id == "revenue-chart").unwrap();
        let expected_chart_w = (940.0_f64 * 0.62).round();
        assert_eq!(
            chart.region.width, expected_chart_w,
            "chart width = main*0.62"
        );

        let metric = p.subtasks.iter().find(|s| s.id == "kpi-metrics").unwrap();
        assert_eq!(metric.region.width, 940.0, "metric width = full main");
    }

    #[test]
    fn normalize_dashboard_height_kept_when_in_range() {
        // chart: inferred=320, LLM=300
        // min=round(320*0.6)=192, max=round(320*1.6)=512
        // 300 ∈ [192,512] → keep 300
        let mut p = dashboard_plan(1200.0);
        let req = dash_req("analytics admin dashboard");
        normalize(&mut p, &req);

        let chart = p.subtasks.iter().find(|s| s.id == "revenue-chart").unwrap();
        assert_eq!(
            chart.region.height, 300.0,
            "LLM height kept when in clamp range"
        );
    }

    #[test]
    fn normalize_dashboard_height_replaced_when_zero() {
        // metric: LLM height=0 (invalid) → use inferred=160
        let mut p = dashboard_plan(1200.0);
        let req = dash_req("data analytics admin dashboard");
        normalize(&mut p, &req);

        let metric = p.subtasks.iter().find(|s| s.id == "kpi-metrics").unwrap();
        assert_eq!(
            metric.region.height, 160.0,
            "zero LLM height replaced by inferred"
        );
    }

    #[test]
    fn normalize_dashboard_height_clamped_to_max() {
        // "customer-table" subtask: id+label match "table" (no "transaction"),
        // so inferred = 340  →  max=round(340*1.6)=544
        // max(round(340*0.6), min(2000, 544)) = max(204, 544) = 544
        let mut p = OrchestratorPlan {
            root_frame: RootFrameSpec {
                id: "root".into(),
                name: "Dashboard".into(),
                width: 1200.0,
                height: 800.0,
                layout: None,
                gap: None,
                padding: None,
                fill: None,
            },
            subtasks: vec![Subtask {
                id: "customer-table".into(),
                label: "Customer Table".into(),
                region: Region {
                    width: 800.0,
                    height: 2000.0,
                },
                id_prefix: String::new(),
                parent_frame_id: None,
                elements: None,
                screen: None,
                generated_root_id: None,
                existing_section_labels: None,
            }],
            style_guide_name: None,
        };
        normalize(&mut p, &dash_req("data analytics admin dashboard"));
        assert_eq!(
            p.subtasks[0].region.height,
            (340.0_f64 * 1.6).round(),
            "over-tall LLM height clamped to max"
        );
    }

    #[test]
    fn normalize_dashboard_height_clamped_to_min() {
        // chart subtask with too-short LLM height (50)
        // inferred=320  →  min=round(320*0.6)=192, max=round(320*1.6)=512
        // max(192, min(50, 512)) = max(192, 50) = 192
        let mut p = OrchestratorPlan {
            root_frame: RootFrameSpec {
                id: "root".into(),
                name: "Dashboard".into(),
                width: 1200.0,
                height: 800.0,
                layout: None,
                gap: None,
                padding: None,
                fill: None,
            },
            subtasks: vec![Subtask {
                id: "rev-chart".into(),
                label: "Revenue Chart".into(),
                region: Region {
                    width: 800.0,
                    height: 50.0,
                },
                id_prefix: String::new(),
                parent_frame_id: None,
                elements: None,
                screen: None,
                generated_root_id: None,
                existing_section_labels: None,
            }],
            style_guide_name: None,
        };
        normalize(&mut p, &dash_req("data analytics admin dashboard"));
        assert_eq!(
            p.subtasks[0].region.height,
            (320.0_f64 * 0.6).round(),
            "too-short LLM height clamped to min"
        );
    }

    #[test]
    fn normalize_non_dashboard_plan_is_unaffected() {
        // A landing page prompt — NOT dashboard-like — must not rewrite regions.
        let mut p = plan(
            1200.0,
            vec![
                subtask("hero", "Hero Section"),
                subtask("features", "Feature Cards"),
            ],
        );
        // Give them non-zero regions so default-fill doesn't fire.
        p.subtasks[0].region = Region {
            width: 1200.0,
            height: 560.0,
        };
        p.subtasks[1].region = Region {
            width: 1200.0,
            height: 400.0,
        };

        normalize(
            &mut p,
            &dash_req("a beautiful landing page for a SaaS product"),
        );

        assert_eq!(p.subtasks[0].region.width, 1200.0);
        assert_eq!(p.subtasks[0].region.height, 560.0);
        assert_eq!(p.subtasks[1].region.width, 1200.0);
        assert_eq!(p.subtasks[1].region.height, 400.0);
    }
}
