from copy import deepcopy
from uuid import uuid4

from backend.database.base import db
from backend.database.crud.message import _get_db_message
from backend.models.chart import ChartPlan

__all__ = ["append_chart_plan", "apply_chart_plan_decision", "update_chart_plan"]


def _append_chart_plan_to_actions(current_actions: dict, chart_plan_entry: dict) -> tuple[dict, dict]:
    """Append a chart plan without mutating the caller's action payload."""
    actions = deepcopy(current_actions) if isinstance(current_actions, dict) else {}
    stored_chart_plans = actions.get("chart_plans")
    chart_plans = stored_chart_plans if isinstance(stored_chart_plans, list) else []
    committed_entry = deepcopy(chart_plan_entry)
    committed_entry["chart_plan_index"] = len(chart_plans)
    chart_plans.append(committed_entry)
    actions["chart_plans"] = chart_plans
    return committed_entry, actions


def append_chart_plan(message_id: str, user_id: str, chart_plan_entry: dict) -> dict | None:
    """Atomically append a chart plan to the latest user-owned message actions."""
    with db.session.begin():
        db_message = _get_db_message(message_id, user_id, for_update=True)
        if not db_message:
            return None
        committed_entry, updated_actions = _append_chart_plan_to_actions(db_message.actions or {}, chart_plan_entry)
        db_message.actions = updated_actions
        db.session.flush()
        return committed_entry


def _update_chart_plan_in_actions(
    current_actions: dict,
    *,
    chart_plan_index: int,
    chart_data: dict,
) -> tuple[dict, dict | None]:
    """Update a chart against the latest actions without mutating the caller's payload."""
    actions = deepcopy(current_actions) if isinstance(current_actions, dict) else {}
    chart_plans = actions.get("chart_plans")
    if not isinstance(chart_plans, list) or chart_plan_index >= len(chart_plans):
        return {"error": "chart_plan_not_found"}, None

    target = chart_plans[chart_plan_index]
    if not isinstance(target, dict) or not isinstance(target.get("chart_plan"), dict):
        return {"error": "chart_plan_payload_missing"}, None

    target["chart_plan"]["chart_type"] = chart_data["kind"]
    target["chart_plan"]["chart_data"] = deepcopy(chart_data)
    target.setdefault("chart_plan_index", chart_plan_index)
    actions["chart_plans"] = chart_plans
    return target, actions


def update_chart_plan(
    message_id: str,
    user_id: str,
    *,
    chart_plan_index: int,
    chart_data: dict,
) -> dict:
    """Atomically update a chart plan in the latest user-owned message actions."""
    with db.session.begin():
        db_message = _get_db_message(message_id, user_id, for_update=True)
        if not db_message:
            return {"error": "message_not_found"}
        result, updated_actions = _update_chart_plan_in_actions(
            db_message.actions or {},
            chart_plan_index=chart_plan_index,
            chart_data=chart_data,
        )
        if updated_actions is None:
            return result
        db_message.actions = updated_actions
        db.session.flush()
        return result


def _apply_chart_plan_decision_to_actions(
    current_actions: dict,
    *,
    chart_plan_index: int,
    action: str,
    proposal_id: str,
    expected_chart_plan: dict | None,
    chart_plan: dict,
) -> tuple[dict, dict | None]:
    """Build the next actions value; return no update for errors and idempotent retries."""
    actions = deepcopy(current_actions) if isinstance(current_actions, dict) else {}
    chart_plans = actions.get("chart_plans")
    if not isinstance(chart_plans, list):
        return {"error": "chart_plan_not_found"}, None
    decisions = actions.get("chart_plan_decisions") or {}
    if not isinstance(decisions, dict):
        return {"error": "invalid_previous_decision"}, None
    previous_decision = decisions.get(proposal_id)
    if previous_decision:
        if not isinstance(previous_decision, dict):
            return {"error": "invalid_previous_decision"}, None
        committed_index = previous_decision.get("chart_plan_index")
        if isinstance(committed_index, int) and 0 <= committed_index < len(chart_plans):
            return {"chart_plan": chart_plans[committed_index], "chart_plans": chart_plans}, None
        return {"error": "invalid_previous_decision"}, None

    if chart_plan_index < 0 or chart_plan_index >= len(chart_plans):
        return {"error": "chart_plan_not_found"}, None

    target = chart_plans[chart_plan_index]
    if not isinstance(target, dict):
        return {"error": "chart_plan_changed"}, None
    target_chart_plan = target.get("chart_plan")
    if not isinstance(target_chart_plan, dict):
        return {"error": "chart_plan_changed"}, None
    target_chart_data = target_chart_plan.get("chart_data")
    if target_chart_data is None:
        normalized_target = None
    else:
        try:
            normalized_target = ChartPlan.model_validate(target_chart_data).model_dump()
        except Exception:
            return {"error": "chart_plan_changed"}, None
    if normalized_target != expected_chart_plan:
        return {"error": "chart_plan_changed"}, None

    proposed = {
        **target,
        "chart_plan": chart_plan,
        "generation_failed": False,
    }
    if action == "replace":
        committed_index = chart_plan_index
        proposed["chart_plan_index"] = committed_index
        chart_plans[committed_index] = proposed
    else:
        version_group_id = target.get("version_group_id") or str(uuid4())
        target["version_group_id"] = version_group_id
        proposed["version_group_id"] = version_group_id
        committed_index = len(chart_plans)
        proposed["chart_plan_index"] = committed_index
        chart_plans.append(proposed)

    decisions[proposal_id] = {
        "action": action,
        "chart_plan_index": committed_index,
    }
    actions["chart_plans"] = chart_plans
    actions["chart_plan_decisions"] = decisions
    return {"chart_plan": proposed, "chart_plans": chart_plans}, actions


def apply_chart_plan_decision(
    message_id: str,
    user_id: str,
    **decision,
) -> dict:
    """Atomically and idempotently apply an AI-generated chart proposal."""
    with db.session.begin():
        db_message = _get_db_message(message_id, user_id, for_update=True)
        if not db_message:
            return {"error": "message_not_found"}
        result, updated_actions = _apply_chart_plan_decision_to_actions(db_message.actions or {}, **decision)
        if updated_actions is None:
            return result
        db_message.actions = updated_actions
        db.session.flush()
        return result
