# SPDX-License-Identifier: AGPL-3.0-only
# Copyright (c) 2026 sol pbc

"""Routes for the owner-facing Suggestions app."""

from __future__ import annotations

from typing import Any

from flask import Blueprint, Response, current_app, jsonify, request

from solstone.apps.curation import copy as curation_copy
from solstone.convey.reasons import (
    ENTITY_BUSY,
    ENTITY_OPERATION_FAILED,
    INVALID_REQUEST_VALUE,
    MISSING_REQUIRED_FIELD,
)
from solstone.convey.utils import error_response, respond_collection
from solstone.think import (
    speaker_candidate_pair_review_candidates,
    speaker_review_candidates,
)
from solstone.think.curation import (
    KIND_ENTITY_AMBIGUITY,
    KIND_ENTITY_MERGE,
    KIND_FACET_CANDIDATE,
    KIND_SPEAKER_CANDIDATE_PAIR,
    KIND_SPEAKER_NAME_VARIANT,
    accept_entity_candidate,
    accept_entity_candidate_batch,
    accept_facet_candidate,
    accept_speaker_candidate,
    accept_speaker_candidate_pair,
    dismiss_entity_candidate,
    dismiss_entity_candidate_batch,
    dismiss_facet_candidate,
    dismiss_speaker_candidate,
    dismiss_speaker_candidate_pair,
    load_open_items,
    merge_preview_fields,
)
from solstone.think.entities import EntityAmbiguityError
from solstone.think.facet_review_candidates import load_candidates
from solstone.think.journal_io import LockTimeout

curation_bp = Blueprint(
    "app:curation",
    __name__,
    url_prefix="/app/curation",
    static_folder="static",
    static_url_path="/static",
)


@curation_bp.route("/")
def index() -> Any:
    return current_app.send_static_file("shell.html")


@curation_bp.route("/api/state")
def api_state() -> Response | tuple[Response, int]:
    try:
        items = load_open_items()
    except EntityAmbiguityError as exc:
        return error_response(ENTITY_OPERATION_FAILED, detail=str(exc))
    return jsonify(
        {
            "facet_items": [
                item.to_dict() for item in items if item.kind == KIND_FACET_CANDIDATE
            ],
            "entity_items": [
                item.to_dict() for item in items if item.kind == KIND_ENTITY_MERGE
            ],
            "ambiguity_items": [
                item.to_dict() for item in items if item.kind == KIND_ENTITY_AMBIGUITY
            ],
            "speaker_items": [
                item.to_dict()
                for item in items
                if item.kind == KIND_SPEAKER_NAME_VARIANT
            ],
            "speaker_candidate_pair_items": [
                item.to_dict()
                for item in items
                if item.kind == KIND_SPEAKER_CANDIDATE_PAIR
            ],
            "copy": curation_copy.curation_copy_payload(),
        }
    )


def _json_body() -> dict[str, Any]:
    data = request.get_json(silent=True) or {}
    return data if isinstance(data, dict) else {}


def _required(data: dict[str, Any], field: str) -> Any:
    value = data.get(field)
    if value is None or value == "":
        raise KeyError(field)
    return value


def _missing_field(field: str) -> tuple[Response, int]:
    return error_response(MISSING_REQUIRED_FIELD, detail=f"Missing {field}")


def _result_response(result: dict[str, Any]) -> Response | tuple[Response, int]:
    if result.get("status") == "error":
        return jsonify(result), 400
    return jsonify(result)


def _speaker_payload(
    data: dict[str, Any],
) -> tuple[str, str, str] | tuple[Response, int]:
    try:
        key = str(_required(data, "key"))
        source_id = str(_required(data, "source_id"))
        target_id = str(_required(data, "target_id"))
    except KeyError as exc:
        return _missing_field(str(exc.args[0]))

    expected = speaker_review_candidates.candidate_key(source_id, target_id)
    if key != expected:
        return error_response(
            INVALID_REQUEST_VALUE,
            detail="key does not match source_id/target_id",
        )
    return key, source_id, target_id


def _speaker_candidate_pair_payload(
    data: dict[str, Any],
) -> tuple[str, str, str] | tuple[Response, int]:
    try:
        key = str(_required(data, "key"))
        anchor_a = str(_required(data, "anchor_a"))
        anchor_b = str(_required(data, "anchor_b"))
    except KeyError as exc:
        return _missing_field(str(exc.args[0]))

    expected = speaker_candidate_pair_review_candidates.candidate_key(
        anchor_a,
        anchor_b,
    )
    if key != expected:
        return error_response(
            INVALID_REQUEST_VALUE,
            detail="key does not match anchor_a/anchor_b",
        )
    return key, anchor_a, anchor_b


@curation_bp.route("/api/facet/candidates")
def facet_candidates() -> Response:
    return respond_collection(load_candidates())


@curation_bp.post("/api/facet/accept")
def accept_facet() -> Response | tuple[Response, int]:
    data = _json_body()
    try:
        name_key = str(_required(data, "name_key"))
    except KeyError:
        return _missing_field("name_key")
    try:
        return _result_response(accept_facet_candidate(name_key))
    except LockTimeout:
        return error_response(ENTITY_BUSY, detail="suggestions are busy; try again")


@curation_bp.post("/api/facet/dismiss")
def dismiss_facet() -> Response | tuple[Response, int]:
    data = _json_body()
    try:
        name_key = str(_required(data, "name_key"))
    except KeyError:
        return _missing_field("name_key")
    try:
        return _result_response(dismiss_facet_candidate(name_key))
    except LockTimeout:
        return error_response(ENTITY_BUSY, detail="suggestions are busy; try again")


@curation_bp.post("/api/entity/preview")
def preview_entity() -> Response | tuple[Response, int]:
    data = _json_body()
    try:
        facet = str(_required(data, "facet"))
        source_slug = str(_required(data, "source_slug"))
        target_slug = str(_required(data, "target_slug"))
    except KeyError as exc:
        return _missing_field(str(exc.args[0]))

    result = accept_entity_candidate(
        facet,
        source_slug,
        target_slug,
        commit=False,
    )
    if result.get("status") == "preview":
        result["preview"] = merge_preview_fields(result.get("merge", {}))
    return _result_response(result)


@curation_bp.post("/api/speaker/preview")
def preview_speaker() -> Response | tuple[Response, int]:
    payload = _speaker_payload(_json_body())
    if not isinstance(payload, tuple) or len(payload) != 3:
        return payload
    _, source_id, target_id = payload

    result = accept_speaker_candidate(
        source_id,
        target_id,
        commit=False,
    )
    if result.get("status") == "preview":
        result["preview"] = merge_preview_fields(result.get("merge", {}))
    return _result_response(result)


@curation_bp.post("/api/speaker/accept")
def accept_speaker() -> Response | tuple[Response, int]:
    payload = _speaker_payload(_json_body())
    if not isinstance(payload, tuple) or len(payload) != 3:
        return payload
    _, source_id, target_id = payload

    try:
        result = accept_speaker_candidate(
            source_id,
            target_id,
            commit=True,
        )
    except LockTimeout:
        return error_response(
            ENTITY_BUSY, detail="speaker suggestions are busy; try again"
        )
    return _result_response(result)


@curation_bp.post("/api/speaker/dismiss")
def dismiss_speaker() -> Response | tuple[Response, int]:
    payload = _speaker_payload(_json_body())
    if not isinstance(payload, tuple) or len(payload) != 3:
        return payload
    _, source_id, target_id = payload

    try:
        result = dismiss_speaker_candidate(source_id, target_id)
    except LockTimeout:
        return error_response(
            ENTITY_BUSY, detail="speaker suggestions are busy; try again"
        )
    return _result_response(result)


@curation_bp.post("/api/speaker-candidate-pair/accept")
def accept_speaker_candidate_pair_route() -> Response | tuple[Response, int]:
    payload = _speaker_candidate_pair_payload(_json_body())
    if not isinstance(payload, tuple) or len(payload) != 3:
        return payload
    _, anchor_a, anchor_b = payload

    try:
        result = accept_speaker_candidate_pair(anchor_a, anchor_b)
    except LockTimeout:
        return error_response(
            ENTITY_BUSY, detail="speaker suggestions are busy; try again"
        )
    return _result_response(result)


@curation_bp.post("/api/speaker-candidate-pair/dismiss")
def dismiss_speaker_candidate_pair_route() -> Response | tuple[Response, int]:
    payload = _speaker_candidate_pair_payload(_json_body())
    if not isinstance(payload, tuple) or len(payload) != 3:
        return payload
    _, anchor_a, anchor_b = payload

    try:
        result = dismiss_speaker_candidate_pair(anchor_a, anchor_b)
    except LockTimeout:
        return error_response(
            ENTITY_BUSY, detail="speaker suggestions are busy; try again"
        )
    return _result_response(result)


@curation_bp.post("/api/entity/accept")
def accept_entity() -> Response | tuple[Response, int]:
    data = _json_body()
    try:
        facet = str(_required(data, "facet"))
        source_slug = str(_required(data, "source_slug"))
        target_slug = str(_required(data, "target_slug"))
    except KeyError as exc:
        return _missing_field(str(exc.args[0]))

    try:
        result = accept_entity_candidate(
            facet,
            source_slug,
            target_slug,
            commit=True,
        )
    except LockTimeout:
        return error_response(
            ENTITY_BUSY, detail="entity merge candidates are busy; try again"
        )
    return _result_response(result)


@curation_bp.post("/api/entity/dismiss")
def dismiss_entity() -> Response | tuple[Response, int]:
    data = _json_body()
    try:
        facet = str(_required(data, "facet"))
        source_slug = str(_required(data, "source_slug"))
        target_slug = str(_required(data, "target_slug"))
    except KeyError as exc:
        return _missing_field(str(exc.args[0]))

    try:
        result = dismiss_entity_candidate(facet, source_slug, target_slug)
    except LockTimeout:
        return error_response(
            ENTITY_BUSY, detail="entity merge candidates are busy; try again"
        )
    return _result_response(result)


@curation_bp.post("/api/entity/accept-batch")
def accept_entity_batch() -> Response | tuple[Response, int]:
    items = _json_body().get("items")
    if not isinstance(items, list) or not items:
        return _missing_field("items")
    return jsonify(accept_entity_candidate_batch(items))


@curation_bp.post("/api/entity/dismiss-batch")
def dismiss_entity_batch() -> Response | tuple[Response, int]:
    items = _json_body().get("items")
    if not isinstance(items, list) or not items:
        return _missing_field("items")
    return jsonify(dismiss_entity_candidate_batch(items))
