"""Visualization + Calculation Fast Lane composition catalog.

Recipes describe product intent above Dataset and Calculation IR. They do not
add App Blocks, Cloud capabilities, Reporting, or entity-backed calculators.

Coverage target (composition artifacts, not primitives):
  Calculator recipes     ~35
  Chart recipes          ~24
  Calculated-chart proofs ~4
  Dataset patterns       7 (see composition_contract.DATASET_COMPOSITION_PATTERNS)
"""

from __future__ import annotations

from typing import Any

from src.domain.app_blocks.composition_contract import (
    COMPOSITION_INVARIANTS,
    DATASET_COMPOSITION_PATTERNS,
    RecipeCalculatorSpec,
    RecipeDatasetDimension,
    RecipeDatasetMeasure,
    RecipeDatasetSpec,
    RecipeFieldSpec,
    RecipePresentationChart,
    RecipePresentationMetric,
    RecipePresentationSpec,
)
from src.domain.app_blocks.vertical_recipes import (
    RecipeBlockSlots,
    RecipeCatalog,
    RecipeRequires,
    VerticalRecipe,
)

PROOF = "VISUALIZATION_CALCULATION"
UNSUPPORTED_CALC = (
    "calculation.evaluate",
    "authoritative_execution",
    "calculator_definition_entity",
    "chart_definition_entity",
    "reporting.dashboard",
)
UNSUPPORTED_CHART = (
    "reporting.dashboard",
    "chart_definition_entity",
    "calculator_definition_entity",
    "bi_warehouse",
    "chart_builder",
)


def _field(
    field_id: str,
    type_name: str,
    *,
    required: bool = True,
    min_value: float | None = None,
    label: str | None = None,
) -> RecipeFieldSpec:
    return RecipeFieldSpec(
        id=field_id,
        type=type_name,  # type: ignore[arg-type]
        required=required,
        min=min_value,
        label=label,
    )


def _calc_recipe(
    *,
    recipe_id: str,
    name: str,
    phrase: str,
    area: str,
    inputs: list[RecipeFieldSpec],
    outputs: list[RecipeFieldSpec],
    author: str = "codegen",
    pack_slug: str | None = None,
    calculator_id: str | None = None,
    dataset_input: str | None = None,
    with_chart: bool = False,
    chart_type: str = "bar",
    chart_dataset: str = "calculated.series",
    metrics: list[str] | None = None,
    heading: str | None = None,
    extra_unsupported: tuple[str, ...] = (),
    extra_phrases: list[str] | None = None,
) -> VerticalRecipe:
    blocks = ["calculation.calculator"]
    terminology: dict[str, RecipeBlockSlots] = {
        "calculation.calculator": RecipeBlockSlots(
            heading=heading or name,
            empty_state="Enter values to calculate.",
            route=f"/{recipe_id.replace('_', '-')}",
        )
    }
    public_labels = {"calculation.calculator": name}
    presentation_metrics = [
        RecipePresentationMetric(output=item) for item in (metrics or [outputs[0].id])
    ]
    charts: list[RecipePresentationChart] = []
    if with_chart:
        blocks.append("visualization.chart")
        terminology["visualization.chart"] = RecipeBlockSlots(
            heading="Calculated chart",
            empty_state="Calculate to see the chart.",
        )
        public_labels["visualization.chart"] = "Calculated chart"
        charts.append(
            RecipePresentationChart(
                dataset=chart_dataset,
                chart_type=chart_type,  # type: ignore[arg-type]
            )
        )
    phrases = [phrase]
    if extra_phrases:
        phrases.extend(extra_phrases)
    return VerticalRecipe(
        id=recipe_id,
        name=name,
        archetype="calculator_operations",
        proof_profile=PROOF,
        intent_phrases=phrases,
        requires=RecipeRequires(app_blocks=blocks),
        terminology=terminology,
        public_labels=public_labels,
        unsupported_assumptions=list(UNSUPPORTED_CALC) + list(extra_unsupported),
        kind="calculation",
        calculator=RecipeCalculatorSpec(
            author=author,  # type: ignore[arg-type]
            execution="local",
            pack_slug=pack_slug,
            calculator_id=calculator_id or recipe_id,
            inputs=inputs,
            outputs=outputs,
            dataset_input=dataset_input,
        ),
        presentation=RecipePresentationSpec(
            primary_block="calculation.calculator",
            metrics=presentation_metrics,
            charts=charts,
        ),
        area=area,
    )


def _chart_recipe(
    *,
    recipe_id: str,
    name: str,
    phrase: str,
    area: str,
    pattern: str,
    entity_key: str,
    chart_type: str,
    dimensions: list[tuple[str, str | None]],
    measures: list[tuple[str, str, str | None]],
    row_budget: int = 100,
    audience: str = "staff",
    heading: str | None = None,
    extra_unsupported: tuple[str, ...] = (),
) -> VerticalRecipe:
    dims = [
        RecipeDatasetDimension(field=field, bucket=bucket)  # type: ignore[arg-type]
        for field, bucket in dimensions
    ]
    meas = [
        RecipeDatasetMeasure(
            id=measure_id,
            aggregate=aggregate,  # type: ignore[arg-type]
            field=field,
        )
        for measure_id, aggregate, field in measures
    ]
    return VerticalRecipe(
        id=recipe_id,
        name=name,
        archetype="visualization_operations",
        proof_profile=PROOF,
        intent_phrases=[phrase],
        requires=RecipeRequires(app_blocks=["visualization.chart"]),
        terminology={
            "visualization.chart": RecipeBlockSlots(
                heading=heading or name,
                empty_state="No series data to chart yet.",
            )
        },
        public_labels={"visualization.chart": name},
        unsupported_assumptions=list(UNSUPPORTED_CHART) + list(extra_unsupported),
        kind="visualization",
        composition_pattern=pattern,
        dataset=RecipeDatasetSpec(
            pattern=pattern,  # type: ignore[arg-type]
            source_kind="records.aggregate",
            entity_key=entity_key,
            dimensions=dims,
            measures=meas,
            audience=audience,  # type: ignore[arg-type]
            freshness="live",
            row_budget=row_budget,
        ),
        presentation=RecipePresentationSpec(
            primary_block="visualization.chart",
            charts=[
                RecipePresentationChart(
                    dataset=f"dataset.{recipe_id}",
                    chart_type=chart_type,  # type: ignore[arg-type]
                    x=dims[0].field if dims else None,
                    y=meas[0].id if meas else None,
                )
            ],
        ),
        area=area,
    )


def _calculated_chart_recipe(
    *,
    recipe_id: str,
    name: str,
    phrase: str,
    area: str,
    inputs: list[RecipeFieldSpec],
    outputs: list[RecipeFieldSpec],
    chart_type: str,
    chart_dataset: str,
    calculator_id: str | None = None,
    metrics: list[str] | None = None,
    extra_unsupported: tuple[str, ...] = (),
) -> VerticalRecipe:
    calc_id = calculator_id or recipe_id
    return VerticalRecipe(
        id=recipe_id,
        name=name,
        archetype="calculator_operations",
        proof_profile=PROOF,
        intent_phrases=[phrase],
        requires=RecipeRequires(
            app_blocks=["calculation.calculator", "visualization.chart"]
        ),
        terminology={
            "calculation.calculator": RecipeBlockSlots(
                heading=name,
                empty_state="Enter values to calculate.",
                route=f"/{recipe_id.replace('_', '-')}",
            ),
            "visualization.chart": RecipeBlockSlots(
                heading="Calculated series",
                empty_state="Calculate to see the chart.",
            ),
        },
        public_labels={
            "calculation.calculator": name,
            "visualization.chart": "Calculated series chart",
        },
        unsupported_assumptions=list(UNSUPPORTED_CALC) + list(extra_unsupported),
        kind="calculated_chart",
        calculator=RecipeCalculatorSpec(
            author="codegen",
            execution="local",
            calculator_id=calc_id,
            inputs=inputs,
            outputs=outputs,
        ),
        dataset=RecipeDatasetSpec(
            pattern="calculation_output",
            source_kind="calculator.emit",
            calculator_id=calc_id,
            audience="public",
            freshness="snapshot",
            row_budget=120,
        ),
        presentation=RecipePresentationSpec(
            primary_block="calculation.calculator",
            metrics=[
                RecipePresentationMetric(output=item)
                for item in (metrics or [outputs[0].id])
            ],
            charts=[
                RecipePresentationChart(
                    dataset=chart_dataset,
                    chart_type=chart_type,  # type: ignore[arg-type]
                )
            ],
        ),
        area=area,
    )


def _calculator_recipes() -> list[VerticalRecipe]:
    def money(i: str, **kw):
        kw.setdefault("min_value", 0)
        return _field(i, "money", **kw)

    def pct(i: str, **kw):
        kw.setdefault("min_value", 0)
        return _field(i, "percent", **kw)

    def num(i: str, **kw):
        kw.setdefault("min_value", 0)
        return _field(i, "number", **kw)

    def integ(i: str, **kw):
        kw.setdefault("min_value", 0)
        return _field(i, "integer", **kw)

    return [
        # Finance / sales
        _calc_recipe(
            recipe_id="loan_payment_calculator",
            name="Loan Payment Calculator",
            phrase="loan payment calculator composition recipe",
            area="finance",
            inputs=[
                money("loan_amount"),
                pct("annual_rate"),
                integ("term_years", min_value=1),
                _field("payment_frequency", "select"),
            ],
            outputs=[
                money("periodic_payment"),
                money("total_interest"),
                money("total_repayment"),
            ],
            metrics=["periodic_payment", "total_interest", "total_repayment"],
            extra_unsupported=("loan_origination_engine",),
        ),
        _calc_recipe(
            recipe_id="discount_calculator",
            name="Discount Calculator",
            phrase="discount calculator composition recipe",
            area="finance",
            inputs=[money("price"), pct("discount_percent"), pct("tax_percent")],
            outputs=[
                money("discounted_subtotal"),
                money("tax_amount"),
                money("final_total"),
            ],
            metrics=["discounted_subtotal", "tax_amount", "final_total"],
        ),
        _calc_recipe(
            recipe_id="sales_commission_calculator",
            name="Sales Commission Calculator",
            phrase="sales commission calculator composition recipe",
            area="crm",
            inputs=[
                money("sale_value"),
                pct("commission_rate"),
                money("bonus", required=False),
            ],
            outputs=[money("commission"), money("payout")],
            dataset_input="salesperson_snapshot",
            metrics=["commission", "payout"],
        ),
        _calc_recipe(
            recipe_id="customer_ltv_calculator",
            name="Customer LTV Calculator",
            phrase="customer lifetime value calculator composition recipe",
            area="crm",
            inputs=[
                money("average_order_value"),
                num("purchase_frequency"),
                num("customer_lifespan_years"),
                pct("gross_margin"),
            ],
            outputs=[money("ltv"), money("gross_ltv")],
            dataset_input="customer_snapshot",
            metrics=["ltv", "gross_ltv"],
        ),
        _calc_recipe(
            recipe_id="weighted_pipeline_calculator",
            name="Weighted Pipeline Calculator",
            phrase="weighted pipeline calculator composition recipe",
            area="crm",
            inputs=[
                money("opportunity_amount"),
                pct("probability"),
            ],
            outputs=[
                money("weighted_pipeline"),
                money("total_pipeline"),
                pct("average_probability"),
            ],
            dataset_input="opportunities",
            with_chart=True,
            chart_type="bar",
            chart_dataset="calculated.weighted_by_stage",
            metrics=["weighted_pipeline", "total_pipeline"],
            extra_unsupported=("crm_analytics_engine",),
        ),
        _calc_recipe(
            recipe_id="lead_value_estimator",
            name="Lead Value Estimator",
            phrase="lead value estimator composition recipe",
            area="crm",
            inputs=[
                integ("leads"),
                pct("conversion_rate"),
                money("average_deal_value"),
            ],
            outputs=[money("expected_pipeline"), integ("expected_deals")],
            metrics=["expected_pipeline", "expected_deals"],
        ),
        _calc_recipe(
            recipe_id="quota_attainment_calculator",
            name="Quota Attainment Calculator",
            phrase="quota attainment calculator composition recipe",
            area="crm",
            inputs=[money("closed_revenue"), money("quota")],
            outputs=[pct("attainment_percent"), money("gap_to_quota")],
            metrics=["attainment_percent", "gap_to_quota"],
        ),
        # SaaS
        _calc_recipe(
            recipe_id="mrr_calculator",
            name="MRR Calculator",
            phrase="mrr calculator composition recipe",
            area="saas",
            inputs=[integ("active_customers"), money("average_monthly_subscription")],
            outputs=[money("mrr"), money("arr")],
            metrics=["mrr", "arr"],
        ),
        _calc_recipe(
            recipe_id="saas_growth_calculator",
            name="SaaS Growth Calculator",
            phrase="saas growth calculator composition recipe",
            area="saas",
            inputs=[
                money("starting_mrr"),
                money("new_mrr"),
                money("expansion_mrr"),
                money("contraction_mrr"),
                money("churned_mrr"),
            ],
            outputs=[
                money("ending_mrr"),
                money("net_new_mrr"),
                pct("growth_rate"),
            ],
            metrics=["ending_mrr", "net_new_mrr", "growth_rate"],
        ),
        _calc_recipe(
            recipe_id="churn_impact_calculator",
            name="Churn Impact Calculator",
            phrase="churn impact calculator composition recipe",
            area="saas",
            inputs=[integ("customers"), money("arpu"), pct("churn_percent")],
            outputs=[
                integ("lost_customers"),
                money("lost_mrr"),
                money("annualized_revenue_impact"),
            ],
            metrics=["lost_customers", "lost_mrr", "annualized_revenue_impact"],
        ),
        _calc_recipe(
            recipe_id="cac_calculator",
            name="CAC Calculator",
            phrase="customer acquisition cost calculator composition recipe",
            area="saas",
            inputs=[
                money("sales_spend"),
                money("marketing_spend"),
                integ("customers_acquired", min_value=1),
            ],
            outputs=[money("cac")],
            metrics=["cac"],
        ),
        _calc_recipe(
            recipe_id="ltv_cac_calculator",
            name="LTV:CAC Calculator",
            phrase="ltv to cac ratio calculator composition recipe",
            area="saas",
            inputs=[money("ltv"), money("cac")],
            outputs=[num("ltv_cac_ratio"), pct("acquisition_efficiency")],
            metrics=["ltv_cac_ratio"],
        ),
        _calc_recipe(
            recipe_id="seat_pricing_calculator",
            name="Seat Pricing Calculator",
            phrase="seat pricing calculator composition recipe",
            area="saas",
            inputs=[
                integ("seats", min_value=1),
                money("price_per_seat"),
                pct("discount_percent"),
                money("add_ons", required=False),
            ],
            outputs=[money("monthly_subtotal"), money("annual_total")],
            metrics=["monthly_subtotal", "annual_total"],
        ),
        _calc_recipe(
            recipe_id="conversion_uplift_calculator",
            name="Conversion Uplift Calculator",
            phrase="conversion uplift calculator composition recipe",
            area="marketing",
            inputs=[
                integ("visitors"),
                pct("baseline_conversion"),
                pct("projected_conversion"),
                money("average_order_value"),
            ],
            outputs=[integ("extra_conversions"), money("revenue_uplift")],
            metrics=["extra_conversions", "revenue_uplift"],
        ),
        # Professional services
        _calc_recipe(
            recipe_id="billable_utilization_calculator",
            name="Billable Utilization Calculator",
            phrase="billable utilization calculator composition recipe",
            area="services",
            inputs=[
                num("available_hours"),
                num("billable_hours"),
                pct("target_utilization"),
                money("hourly_rate"),
            ],
            outputs=[
                pct("utilization_percent"),
                pct("utilization_gap"),
                money("billable_revenue"),
                num("target_billable_hours"),
            ],
            dataset_input="time_entries",
            metrics=["utilization_percent", "billable_revenue"],
        ),
        _calc_recipe(
            recipe_id="staff_capacity_calculator",
            name="Staff Capacity Calculator",
            phrase="staff capacity calculator composition recipe",
            area="services",
            inputs=[
                integ("staff_count", min_value=1),
                num("working_hours"),
                pct("availability_percent"),
                num("required_hours"),
            ],
            outputs=[num("available_capacity"), num("capacity_gap")],
            with_chart=True,
            chart_type="bar",
            chart_dataset="calculated.capacity_comparison",
            metrics=["available_capacity", "capacity_gap"],
        ),
        _calc_recipe(
            recipe_id="hourly_rate_calculator",
            name="Hourly Rate Calculator",
            phrase="hourly rate calculator composition recipe",
            area="services",
            inputs=[
                money("target_income"),
                num("billable_hours_per_year"),
                pct("overhead_percent"),
            ],
            outputs=[money("suggested_hourly_rate")],
            metrics=["suggested_hourly_rate"],
        ),
        _calc_recipe(
            recipe_id="retainer_pricing_calculator",
            name="Retainer Pricing Calculator",
            phrase="retainer pricing calculator composition recipe",
            area="services",
            inputs=[
                num("hours_per_month"),
                money("hourly_rate"),
                pct("retainer_discount"),
            ],
            outputs=[money("monthly_retainer"), money("annual_retainer")],
            metrics=["monthly_retainer", "annual_retainer"],
        ),
        _calc_recipe(
            recipe_id="consulting_fee_estimator",
            name="Consulting Fee Estimator",
            phrase="consulting fee estimator composition recipe",
            area="services",
            inputs=[
                num("estimated_hours"),
                money("hourly_rate"),
                money("expenses", required=False),
                pct("contingency_percent"),
            ],
            outputs=[money("proposed_fee"), money("contingency_amount")],
            metrics=["proposed_fee"],
        ),
        _calc_recipe(
            recipe_id="project_budget_burn_calculator",
            name="Project Budget Burn Calculator",
            phrase="project budget burn calculator composition recipe",
            area="services",
            inputs=[
                money("budget"),
                money("spent"),
                num("elapsed_days"),
                num("planned_days"),
            ],
            outputs=[
                money("remaining_budget"),
                pct("burn_percent"),
                pct("schedule_percent"),
            ],
            dataset_input="project_costs",
            metrics=["remaining_budget", "burn_percent"],
        ),
        # Booking
        _calc_recipe(
            recipe_id="booking_capacity_calculator",
            name="Booking Capacity Calculator",
            phrase="booking capacity calculator composition recipe",
            area="booking",
            inputs=[
                integ("staff_count", min_value=1),
                num("working_hours_per_day"),
                integ("service_duration_minutes", min_value=1),
                integ("working_days", min_value=1),
                pct("utilization_target"),
            ],
            outputs=[
                integ("appointments_per_day"),
                integ("appointments_per_month"),
                integ("target_capacity"),
            ],
            metrics=["appointments_per_day", "target_capacity"],
            extra_unsupported=("booking_analytics_engine",),
        ),
        _calc_recipe(
            recipe_id="appointment_revenue_estimator",
            name="Appointment Revenue Estimator",
            phrase="appointment revenue estimator composition recipe",
            area="booking",
            inputs=[
                integ("appointments"),
                money("average_service_price"),
                pct("no_show_rate"),
            ],
            outputs=[money("gross_revenue"), money("expected_revenue")],
            metrics=["gross_revenue", "expected_revenue"],
        ),
        _calc_recipe(
            recipe_id="occupancy_calculator",
            name="Occupancy Calculator",
            phrase="occupancy calculator composition recipe",
            area="booking",
            inputs=[integ("booked_slots"), integ("available_slots", min_value=1)],
            outputs=[pct("occupancy_percent")],
            metrics=["occupancy_percent"],
        ),
        _calc_recipe(
            recipe_id="no_show_impact_calculator",
            name="No-Show Impact Calculator",
            phrase="no show impact calculator composition recipe",
            area="booking",
            inputs=[
                integ("scheduled_appointments"),
                pct("no_show_rate"),
                money("average_service_price"),
            ],
            outputs=[integ("expected_no_shows"), money("lost_revenue")],
            metrics=["expected_no_shows", "lost_revenue"],
        ),
        # Inventory / commerce
        _calc_recipe(
            recipe_id="stock_cover_calculator",
            name="Stock Cover Calculator",
            phrase="stock cover calculator composition recipe",
            area="inventory",
            inputs=[
                num("quantity_on_hand"),
                num("average_daily_usage"),
                num("incoming_quantity", required=False),
            ],
            outputs=[num("days_of_stock"), num("days_of_stock_after_incoming")],
            dataset_input="inventory_snapshot",
            metrics=["days_of_stock"],
        ),
        _calc_recipe(
            recipe_id="inventory_reorder_calculator",
            name="Inventory Reorder Calculator",
            phrase="inventory reorder calculator composition recipe",
            area="inventory",
            inputs=[
                num("average_daily_usage"),
                num("lead_time_days"),
                num("safety_stock"),
            ],
            outputs=[num("reorder_point"), num("suggested_order_qty")],
            metrics=["reorder_point", "suggested_order_qty"],
        ),
        _calc_recipe(
            recipe_id="product_margin_calculator",
            name="Product Margin Calculator",
            phrase="product margin calculator composition recipe",
            area="inventory",
            inputs=[money("selling_price"), money("unit_cost")],
            outputs=[money("unit_profit"), pct("margin_percent"), pct("markup_percent")],
            metrics=["unit_profit", "margin_percent"],
        ),
        _calc_recipe(
            recipe_id="markup_calculator",
            name="Markup Calculator",
            phrase="markup calculator composition recipe",
            area="inventory",
            inputs=[money("cost"), pct("markup_percent")],
            outputs=[money("selling_price"), money("markup_amount")],
            metrics=["selling_price", "markup_amount"],
        ),
        _calc_recipe(
            recipe_id="shipping_cost_estimator",
            name="Shipping Cost Estimator",
            phrase="shipping cost estimator composition recipe",
            area="inventory",
            inputs=[
                num("weight_kg"),
                num("distance_km"),
                money("base_rate"),
                money("per_kg_rate"),
            ],
            outputs=[money("estimated_shipping")],
            metrics=["estimated_shipping"],
        ),
        # Marketing
        _calc_recipe(
            recipe_id="roas_calculator",
            name="ROAS Calculator",
            phrase="roas calculator composition recipe",
            area="marketing",
            inputs=[money("ad_spend"), money("attributed_revenue")],
            outputs=[num("roas"), pct("roi")],
            with_chart=True,
            chart_type="bar",
            chart_dataset="calculated.spend_vs_revenue",
            metrics=["roas", "roi"],
        ),
        _calc_recipe(
            recipe_id="ad_budget_calculator",
            name="Ad Budget Calculator",
            phrase="ad budget calculator composition recipe",
            area="marketing",
            inputs=[
                money("revenue_goal"),
                num("target_roas"),
                pct("margin_percent"),
            ],
            outputs=[money("suggested_ad_budget")],
            metrics=["suggested_ad_budget"],
        ),
        _calc_recipe(
            recipe_id="cpc_calculator",
            name="CPC Calculator",
            phrase="cost per click calculator composition recipe",
            area="marketing",
            inputs=[money("ad_spend"), integ("clicks", min_value=1)],
            outputs=[money("cpc")],
            metrics=["cpc"],
        ),
        _calc_recipe(
            recipe_id="lead_cost_calculator",
            name="Lead Cost Calculator",
            phrase="lead cost calculator composition recipe",
            area="marketing",
            inputs=[money("ad_spend"), integ("leads", min_value=1)],
            outputs=[money("cost_per_lead")],
            metrics=["cost_per_lead"],
        ),
        _calc_recipe(
            recipe_id="email_campaign_roi_calculator",
            name="Email Campaign ROI Calculator",
            phrase="email campaign roi calculator composition recipe",
            area="marketing",
            inputs=[
                money("campaign_cost"),
                integ("conversions"),
                money("average_order_value"),
            ],
            outputs=[money("campaign_revenue"), pct("campaign_roi")],
            metrics=["campaign_revenue", "campaign_roi"],
        ),
        # Property
        _calc_recipe(
            recipe_id="rental_yield_calculator",
            name="Rental Yield Calculator",
            phrase="rental yield calculator composition recipe",
            area="property",
            inputs=[
                money("property_value"),
                money("monthly_rent"),
                money("annual_expenses"),
            ],
            outputs=[
                money("annual_rent"),
                pct("gross_yield"),
                money("net_income"),
                pct("net_yield"),
            ],
            metrics=["gross_yield", "net_yield"],
            extra_unsupported=("property_valuation_engine",),
        ),
        _calc_recipe(
            recipe_id="cap_rate_calculator",
            name="Cap Rate Calculator",
            phrase="cap rate calculator composition recipe",
            area="property",
            inputs=[money("net_operating_income"), money("property_value")],
            outputs=[pct("cap_rate")],
            metrics=["cap_rate"],
        ),
        _calc_recipe(
            recipe_id="property_roi_calculator",
            name="Property ROI Calculator",
            phrase="property roi calculator composition recipe",
            area="property",
            inputs=[
                money("purchase_price"),
                money("total_investment"),
                money("annual_cash_flow"),
            ],
            outputs=[pct("cash_on_cash_return"), pct("simple_roi")],
            metrics=["cash_on_cash_return", "simple_roi"],
        ),
        _calc_recipe(
            recipe_id="rent_affordability_calculator",
            name="Rent Affordability Calculator",
            phrase="rent affordability calculator composition recipe",
            area="property",
            inputs=[money("monthly_income"), pct("rent_to_income_target")],
            outputs=[money("max_affordable_rent")],
            metrics=["max_affordable_rent"],
        ),
        _calc_recipe(
            recipe_id="vacancy_loss_calculator",
            name="Vacancy Loss Calculator",
            phrase="vacancy loss calculator composition recipe",
            area="property",
            inputs=[
                money("monthly_rent"),
                integ("vacant_days"),
                integ("days_in_period", min_value=1),
            ],
            outputs=[money("vacancy_loss")],
            metrics=["vacancy_loss"],
        ),
        # Education
        _calc_recipe(
            recipe_id="tuition_cost_estimator",
            name="Tuition Cost Estimator",
            phrase="tuition cost estimator composition recipe",
            area="education",
            inputs=[
                money("tuition_per_term"),
                integ("terms"),
                money("fees", required=False),
                money("scholarships", required=False),
            ],
            outputs=[money("total_tuition"), money("net_cost")],
            metrics=["total_tuition", "net_cost"],
        ),
        _calc_recipe(
            recipe_id="gpa_calculator",
            name="GPA Calculator",
            phrase="gpa calculator composition recipe",
            area="education",
            inputs=[
                num("grade_points"),
                num("credit_hours", min_value=0.1),
            ],
            outputs=[num("gpa")],
            metrics=["gpa"],
        ),
        _calc_recipe(
            recipe_id="grade_calculator",
            name="Grade Calculator",
            phrase="weighted grade calculator composition recipe",
            area="education",
            inputs=[
                pct("assignment_score"),
                pct("assignment_weight"),
                pct("exam_score"),
                pct("exam_weight"),
            ],
            outputs=[pct("final_grade")],
            metrics=["final_grade"],
        ),
        _calc_recipe(
            recipe_id="attendance_percentage_calculator",
            name="Attendance Percentage Calculator",
            phrase="attendance percentage calculator composition recipe",
            area="education",
            inputs=[integ("sessions_attended"), integ("sessions_total", min_value=1)],
            outputs=[pct("attendance_percent")],
            metrics=["attendance_percent"],
        ),
        # Compliance / case ops
        _calc_recipe(
            recipe_id="corrective_action_progress_calculator",
            name="Corrective Action Progress Calculator",
            phrase="corrective action progress calculator composition recipe",
            area="compliance",
            inputs=[
                integ("actions_closed"),
                integ("actions_total", min_value=1),
            ],
            outputs=[pct("completion_percent"), integ("actions_remaining")],
            metrics=["completion_percent", "actions_remaining"],
        ),
        _calc_recipe(
            recipe_id="case_resolution_rate_calculator",
            name="Case Resolution Rate Calculator",
            phrase="case resolution rate calculator composition recipe",
            area="compliance",
            inputs=[integ("cases_closed"), integ("cases_opened", min_value=1)],
            outputs=[pct("resolution_rate")],
            metrics=["resolution_rate"],
        ),
    ]


def _chart_recipes() -> list[VerticalRecipe]:
    return [
        # Operational / generic patterns
        _chart_recipe(
            recipe_id="records_by_status",
            name="Records by Status",
            phrase="records by status chart composition recipe",
            area="operations",
            pattern="distribution",
            entity_key="record",
            chart_type="bar",
            dimensions=[("status", None)],
            measures=[("count", "count", None)],
        ),
        _chart_recipe(
            recipe_id="records_created_over_time",
            name="Records Created Over Time",
            phrase="records created over time chart composition recipe",
            area="operations",
            pattern="trend",
            entity_key="record",
            chart_type="line",
            dimensions=[("created_at", "month")],
            measures=[("created_count", "count", None)],
        ),
        _chart_recipe(
            recipe_id="records_value_over_time",
            name="Records Value Over Time",
            phrase="records value over time chart composition recipe",
            area="operations",
            pattern="value_trend",
            entity_key="record",
            chart_type="line",
            dimensions=[("created_at", "month")],
            measures=[("value_sum", "sum", "amount")],
        ),
        _chart_recipe(
            recipe_id="records_by_category",
            name="Records by Category",
            phrase="records by category chart composition recipe",
            area="operations",
            pattern="distribution",
            entity_key="record",
            chart_type="pie",
            dimensions=[("category", None)],
            measures=[("count", "count", None)],
        ),
        _chart_recipe(
            recipe_id="records_value_by_category",
            name="Records Value by Category",
            phrase="records value by category chart composition recipe",
            area="operations",
            pattern="value_distribution",
            entity_key="record",
            chart_type="bar",
            dimensions=[("category", None)],
            measures=[("value_sum", "sum", "amount")],
        ),
        _chart_recipe(
            recipe_id="workload_by_assignee",
            name="Workload by Assignee",
            phrase="workload by assignee chart composition recipe",
            area="projects",
            pattern="comparison",
            entity_key="task",
            chart_type="bar",
            dimensions=[("assignee", None)],
            measures=[("open_tasks", "count", None)],
        ),
        _chart_recipe(
            recipe_id="completion_trend",
            name="Completion Trend",
            phrase="completion trend chart composition recipe",
            area="projects",
            pattern="trend",
            entity_key="task",
            chart_type="area",
            dimensions=[("completed_at", "week")],
            measures=[("completed_count", "count", None)],
        ),
        _chart_recipe(
            recipe_id="open_vs_closed",
            name="Open vs Closed",
            phrase="open versus closed chart composition recipe",
            area="operations",
            pattern="distribution",
            entity_key="record",
            chart_type="pie",
            dimensions=[("state", None)],
            measures=[("count", "count", None)],
        ),
        _chart_recipe(
            recipe_id="priority_distribution",
            name="Priority Distribution",
            phrase="priority distribution chart composition recipe",
            area="operations",
            pattern="distribution",
            entity_key="task",
            chart_type="bar",
            dimensions=[("priority", None)],
            measures=[("count", "count", None)],
        ),
        _chart_recipe(
            recipe_id="aging_distribution",
            name="Aging Distribution",
            phrase="aging distribution chart composition recipe",
            area="operations",
            pattern="distribution",
            entity_key="case",
            chart_type="bar",
            dimensions=[("age_bucket", None)],
            measures=[("count", "count", None)],
        ),
        # CRM
        _chart_recipe(
            recipe_id="sales_pipeline_by_stage",
            name="Sales Pipeline by Stage",
            phrase="sales pipeline by stage chart composition recipe",
            area="crm",
            pattern="value_distribution",
            entity_key="crm.opportunity",
            chart_type="bar",
            dimensions=[("stage", None)],
            measures=[("pipeline_value", "sum", "amount")],
        ),
        _chart_recipe(
            recipe_id="leads_created_over_time",
            name="Leads Created Over Time",
            phrase="leads created over time chart composition recipe",
            area="crm",
            pattern="trend",
            entity_key="crm.lead",
            chart_type="line",
            dimensions=[("created_at", "week")],
            measures=[("lead_count", "count", None)],
        ),
        _chart_recipe(
            recipe_id="lead_source_distribution",
            name="Lead Source Distribution",
            phrase="lead source distribution chart composition recipe",
            area="crm",
            pattern="distribution",
            entity_key="crm.lead",
            chart_type="pie",
            dimensions=[("lead_source", None)],
            measures=[("count", "count", None)],
        ),
        _chart_recipe(
            recipe_id="revenue_by_sales_rep",
            name="Revenue by Sales Rep",
            phrase="revenue by sales rep chart composition recipe",
            area="crm",
            pattern="comparison",
            entity_key="crm.opportunity",
            chart_type="bar",
            dimensions=[("owner", None)],
            measures=[("closed_value", "sum", "closed_value")],
        ),
        _chart_recipe(
            recipe_id="deals_won_over_time",
            name="Deals Won Over Time",
            phrase="deals won over time chart composition recipe",
            area="crm",
            pattern="trend",
            entity_key="crm.opportunity",
            chart_type="line",
            dimensions=[("won_at", "month")],
            measures=[("won_count", "count", None)],
        ),
        _chart_recipe(
            recipe_id="leads_by_status",
            name="Leads by Status",
            phrase="leads by status chart composition recipe",
            area="crm",
            pattern="distribution",
            entity_key="crm.lead",
            chart_type="bar",
            dimensions=[("status", None)],
            measures=[("count", "count", None)],
        ),
        _chart_recipe(
            recipe_id="opportunities_created_over_time",
            name="Opportunities Created Over Time",
            phrase="opportunities created over time chart composition recipe",
            area="crm",
            pattern="trend",
            entity_key="crm.opportunity",
            chart_type="area",
            dimensions=[("created_at", "month")],
            measures=[("created_count", "count", None)],
        ),
        # Projects
        _chart_recipe(
            recipe_id="tasks_by_status",
            name="Tasks by Status",
            phrase="tasks by status chart composition recipe",
            area="projects",
            pattern="distribution",
            entity_key="task",
            chart_type="bar",
            dimensions=[("status", None)],
            measures=[("count", "count", None)],
        ),
        _chart_recipe(
            recipe_id="tasks_by_priority",
            name="Tasks by Priority",
            phrase="tasks by priority chart composition recipe",
            area="projects",
            pattern="distribution",
            entity_key="task",
            chart_type="pie",
            dimensions=[("priority", None)],
            measures=[("count", "count", None)],
        ),
        _chart_recipe(
            recipe_id="tasks_completed_over_time",
            name="Tasks Completed Over Time",
            phrase="tasks completed over time chart composition recipe",
            area="projects",
            pattern="trend",
            entity_key="task",
            chart_type="line",
            dimensions=[("completed_at", "week")],
            measures=[("completed_count", "count", None)],
        ),
        _chart_recipe(
            recipe_id="project_budget_vs_actual",
            name="Project Budget vs Actual",
            phrase="project budget versus actual chart composition recipe",
            area="projects",
            pattern="comparison",
            entity_key="project",
            chart_type="bar",
            dimensions=[("category", None)],
            measures=[("amount", "sum", "amount")],
        ),
        _chart_recipe(
            recipe_id="project_hours_by_category",
            name="Project Hours by Category",
            phrase="project hours by category chart composition recipe",
            area="projects",
            pattern="value_distribution",
            entity_key="time_entry",
            chart_type="bar",
            dimensions=[("category", None)],
            measures=[("hours_sum", "sum", "hours")],
        ),
        # Booking
        _chart_recipe(
            recipe_id="appointments_over_time",
            name="Appointments Over Time",
            phrase="appointments over time chart composition recipe",
            area="booking",
            pattern="trend",
            entity_key="booking.appointment",
            chart_type="line",
            dimensions=[("appointment_date", "day")],
            measures=[("appointment_count", "count", None)],
        ),
        _chart_recipe(
            recipe_id="appointments_by_status",
            name="Appointments by Status",
            phrase="appointments by status chart composition recipe",
            area="booking",
            pattern="distribution",
            entity_key="booking.appointment",
            chart_type="pie",
            dimensions=[("status", None)],
            measures=[("count", "count", None)],
        ),
        _chart_recipe(
            recipe_id="bookings_by_service",
            name="Bookings by Service",
            phrase="bookings by service chart composition recipe",
            area="booking",
            pattern="distribution",
            entity_key="booking.appointment",
            chart_type="bar",
            dimensions=[("service", None)],
            measures=[("count", "count", None)],
        ),
        _chart_recipe(
            recipe_id="bookings_by_staff",
            name="Bookings by Staff",
            phrase="bookings by staff chart composition recipe",
            area="booking",
            pattern="comparison",
            entity_key="booking.appointment",
            chart_type="bar",
            dimensions=[("staff", None)],
            measures=[("count", "count", None)],
        ),
        _chart_recipe(
            recipe_id="occupancy_over_time",
            name="Occupancy Over Time",
            phrase="occupancy over time chart composition recipe",
            area="booking",
            pattern="value_trend",
            entity_key="booking.appointment",
            chart_type="area",
            dimensions=[("appointment_date", "day")],
            measures=[("occupancy_pct", "avg", "occupancy_percent")],
        ),
        # Inventory
        _chart_recipe(
            recipe_id="stock_by_item",
            name="Stock by Item",
            phrase="stock by item chart composition recipe",
            area="inventory",
            pattern="comparison",
            entity_key="inventory.stock",
            chart_type="bar",
            dimensions=[("item_name", None)],
            measures=[("quantity_on_hand", "sum", "quantity_on_hand")],
            row_budget=20,
        ),
        _chart_recipe(
            recipe_id="stock_by_location",
            name="Stock by Location",
            phrase="stock by location chart composition recipe",
            area="inventory",
            pattern="comparison",
            entity_key="inventory.stock",
            chart_type="bar",
            dimensions=[("location", None)],
            measures=[("quantity_on_hand", "sum", "quantity_on_hand")],
        ),
        _chart_recipe(
            recipe_id="inventory_value_by_category",
            name="Inventory Value by Category",
            phrase="inventory value by category chart composition recipe",
            area="inventory",
            pattern="value_distribution",
            entity_key="inventory.stock",
            chart_type="bar",
            dimensions=[("category", None)],
            measures=[("inventory_value", "sum", "value")],
        ),
        _chart_recipe(
            recipe_id="low_stock_distribution",
            name="Low Stock Distribution",
            phrase="low stock distribution chart composition recipe",
            area="inventory",
            pattern="distribution",
            entity_key="inventory.stock",
            chart_type="pie",
            dimensions=[("stock_band", None)],
            measures=[("count", "count", None)],
        ),
        _chart_recipe(
            recipe_id="inventory_movement_over_time",
            name="Inventory Movement Over Time",
            phrase="inventory movement over time chart composition recipe",
            area="inventory",
            pattern="trend",
            entity_key="inventory.movement",
            chart_type="line",
            dimensions=[("moved_at", "week")],
            measures=[("movement_count", "count", None)],
        ),
        # Support / case / compliance
        _chart_recipe(
            recipe_id="cases_by_status",
            name="Cases by Status",
            phrase="cases by status chart composition recipe",
            area="compliance",
            pattern="distribution",
            entity_key="case",
            chart_type="bar",
            dimensions=[("status", None)],
            measures=[("count", "count", None)],
        ),
        _chart_recipe(
            recipe_id="cases_by_priority",
            name="Cases by Priority",
            phrase="cases by priority chart composition recipe",
            area="compliance",
            pattern="distribution",
            entity_key="case",
            chart_type="pie",
            dimensions=[("priority", None)],
            measures=[("count", "count", None)],
        ),
        _chart_recipe(
            recipe_id="cases_created_over_time",
            name="Cases Created Over Time",
            phrase="cases created over time chart composition recipe",
            area="compliance",
            pattern="trend",
            entity_key="case",
            chart_type="line",
            dimensions=[("created_at", "week")],
            measures=[("created_count", "count", None)],
        ),
        _chart_recipe(
            recipe_id="cases_by_assignee",
            name="Cases by Assignee",
            phrase="cases by assignee chart composition recipe",
            area="compliance",
            pattern="comparison",
            entity_key="case",
            chart_type="bar",
            dimensions=[("assignee", None)],
            measures=[("count", "count", None)],
        ),
        _chart_recipe(
            recipe_id="findings_by_severity",
            name="Findings by Severity",
            phrase="findings by severity chart composition recipe",
            area="compliance",
            pattern="distribution",
            entity_key="audit_finding",
            chart_type="bar",
            dimensions=[("severity", None)],
            measures=[("count", "count", None)],
        ),
        _chart_recipe(
            recipe_id="findings_by_status",
            name="Findings by Status",
            phrase="findings by status chart composition recipe",
            area="compliance",
            pattern="distribution",
            entity_key="audit_finding",
            chart_type="pie",
            dimensions=[("status", None)],
            measures=[("count", "count", None)],
        ),
        # Marketing / education / property (lighter)
        _chart_recipe(
            recipe_id="campaign_performance_over_time",
            name="Campaign Performance Over Time",
            phrase="campaign performance over time chart composition recipe",
            area="marketing",
            pattern="value_trend",
            entity_key="marketing.campaign",
            chart_type="line",
            dimensions=[("period", "week")],
            measures=[("attributed_revenue", "sum", "revenue")],
        ),
        _chart_recipe(
            recipe_id="students_by_status",
            name="Students by Status",
            phrase="students by status chart composition recipe",
            area="education",
            pattern="distribution",
            entity_key="student",
            chart_type="bar",
            dimensions=[("status", None)],
            measures=[("count", "count", None)],
        ),
        _chart_recipe(
            recipe_id="maintenance_requests_by_status",
            name="Maintenance Requests by Status",
            phrase="maintenance requests by status chart composition recipe",
            area="property",
            pattern="distribution",
            entity_key="maintenance_request",
            chart_type="bar",
            dimensions=[("status", None)],
            measures=[("count", "count", None)],
        ),
    ]


def _calculated_chart_proofs() -> list[VerticalRecipe]:
    def money(i: str, **kw):
        kw.setdefault("min_value", 0)
        return _field(i, "money", **kw)

    def pct(i: str, **kw):
        kw.setdefault("min_value", 0)
        return _field(i, "percent", **kw)

    def integ(i: str, **kw):
        kw.setdefault("min_value", 0)
        return _field(i, "integer", **kw)

    def num(i: str, **kw):
        kw.setdefault("min_value", 0)
        return _field(i, "number", **kw)

    return [
        _calculated_chart_recipe(
            recipe_id="break_even_calculator",
            name="Break-Even Calculator",
            phrase="break even calculator with cost revenue chart composition recipe",
            area="finance",
            inputs=[
                money("fixed_costs"),
                money("unit_price"),
                money("variable_unit_cost"),
            ],
            outputs=[
                integ("break_even_units"),
                money("break_even_revenue"),
            ],
            chart_type="line",
            chart_dataset="calculated.break_even_series",
            metrics=["break_even_units", "break_even_revenue"],
            extra_unsupported=("accounting_engine",),
        ),
        _calculated_chart_recipe(
            recipe_id="mortgage_amortization_chart",
            name="Mortgage Amortization Chart",
            phrase="mortgage amortization chart composition recipe",
            area="property",
            inputs=[
                money("principal"),
                pct("annual_rate"),
                integ("term_years", min_value=1),
            ],
            outputs=[
                money("monthly_payment"),
                money("total_interest"),
                money("total_paid"),
            ],
            chart_type="area",
            chart_dataset="calculated.amortization",
            calculator_id="mortgage_payment_calculator",
            metrics=["monthly_payment", "total_interest", "total_paid"],
            extra_unsupported=("loan_origination_engine", "amortization_builtin"),
        ),
        _calculated_chart_recipe(
            recipe_id="roi_payback_chart",
            name="ROI Payback Chart",
            phrase="roi payback chart composition recipe",
            area="finance",
            inputs=[
                money("investment"),
                money("monthly_return"),
                integ("months", min_value=1),
            ],
            outputs=[pct("roi_percent"), integ("payback_months")],
            chart_type="line",
            chart_dataset="calculated.cumulative_return",
            metrics=["roi_percent", "payback_months"],
            extra_unsupported=("npv_builtin", "irr_builtin"),
        ),
        _calculated_chart_recipe(
            recipe_id="capacity_gap_chart",
            name="Capacity Gap Chart",
            phrase="capacity gap chart composition recipe",
            area="services",
            inputs=[
                num("required_hours"),
                integ("staff_count", min_value=1),
                num("hours_per_staff"),
            ],
            outputs=[num("available_capacity"), num("capacity_gap")],
            chart_type="bar",
            chart_dataset="calculated.capacity_bars",
            metrics=["available_capacity", "capacity_gap"],
        ),
    ]


def composition_catalog() -> RecipeCatalog:
    recipes = _calculator_recipes() + _chart_recipes() + _calculated_chart_proofs()
    return RecipeCatalog(
        schema_version=1,
        lane="fast_lane",
        batch="visualization_calculation_catalog",
        ready_for_fast_lane_composition=True,
        recipes=recipes,
    )


def composition_coverage_summary() -> dict[str, Any]:
    catalog = composition_catalog()
    by_kind: dict[str, int] = {}
    by_area: dict[str, int] = {}
    for recipe in catalog.recipes:
        kind = recipe.kind or "unknown"
        by_kind[kind] = by_kind.get(kind, 0) + 1
        area = recipe.area or "unspecified"
        by_area[area] = by_area.get(area, 0) + 1
    return {
        "batch": catalog.batch,
        "recipe_count": len(catalog.recipes),
        "by_kind": by_kind,
        "by_area": by_area,
        "dataset_patterns": list(DATASET_COMPOSITION_PATTERNS),
        "invariants": list(COMPOSITION_INVARIANTS),
        "batch_10_overlap": [
            "mortgage_payment_calculator",
            "project_margin_calculator",
            "roi_estimator",
            "dataset_bar_chart",
        ],
    }
