"""Tool: ``update_finding``. Mutate an existing finding by id."""

from __future__ import annotations

from typing import Any

from langgraph.config import get_config

from ..reviewer_findings import (
    DEFAULT_FINDING_TITLE,
    MAX_SUGGESTION_LINES,
    Finding,
    ReviewerThreadMissingError,
    clip_suggestion,
    get_thread_id_from_runtime,
    list_findings,
    normalize_finding_title,
    resolve_review_head_sha,
    thread_missing_tool_result,
    update_finding_fields,
)
from ..utils.reviewer_outcomes import emit_finding_status_outcome


def _is_non_empty_str(value: Any) -> bool:
    return isinstance(value, str) and bool(value)


def _normalize_note(note: str | None) -> str | None:
    if note is None:
        return None
    normalized = note.strip()
    return normalized or None


def _has_published_github_surface(finding: Finding) -> bool:
    surface = finding.get("surface")
    if isinstance(surface, dict) and (
        isinstance(surface.get("github_review_comment_id"), int)
        or _is_non_empty_str(surface.get("github_review_thread_id"))
    ):
        return True
    comment_ids = finding.get("github_review_comment_ids")
    thread_ids = finding.get("github_review_thread_ids")
    return (
        isinstance(finding.get("github_review_comment_id"), int)
        or _is_non_empty_str(finding.get("github_review_thread_id"))
        or (isinstance(comment_ids, list) and any(isinstance(item, int) for item in comment_ids))
        or (isinstance(thread_ids, list) and any(_is_non_empty_str(item) for item in thread_ids))
    )


async def update_finding(
    finding_id: str,
    status: str | None = None,
    severity: str | None = None,
    confidence: str | None = None,
    title: str | None = None,
    description: str | None = None,
    suggestion: str | None = None,
    note: str | None = None,
) -> dict[str, Any]:
    """Update fields on an existing finding.

    Use this on a re-review run to mark an existing finding as resolved or
    dismissed, or to revise its severity/description/suggestion if the new
    commits changed the situation.

    Args:
        finding_id: The id returned by ``add_finding`` (or shown in the
            ``Existing findings`` block of the re-review user message).
        status: New status (``open``, ``resolved``, ``dismissed``).
            Use ``resolved`` when the new commits address the issue. Resolving
            or dismissing requires a ``note`` with the full message to post.
        severity: New severity, if reassessing.
        confidence: New confidence rating (``low``, ``medium``, ``high``), if
            new commits change how sure you are the finding is a real issue.
        title: New concise generated headline, if revising.
        description: New description body, if revising. Do not repeat ``title``
            as the first line.
        suggestion: New replacement text. Pass an empty string to clear it.
            Capped at 4 lines — longer values are dropped (the finding keeps
            its description). Only set this for small, obvious fixes.
        note: Optional free-form note explaining the change. Required when
            resolving or dismissing because it is posted verbatim as the full
            GitHub reply body.

    Returns:
        Dictionary with ``success`` and (on success) the updated ``finding``.
    """
    if status is not None and status not in {"open", "resolved", "dismissed"}:
        return {"success": False, "error": f"Invalid status: {status}"}
    if severity is not None and severity not in {"low", "medium", "high", "critical"}:
        return {"success": False, "error": f"Invalid severity: {severity}"}
    if confidence is not None and confidence not in {"low", "medium", "high"}:
        return {"success": False, "error": f"Invalid confidence: {confidence}"}
    normalized_note = _normalize_note(note)
    if status in {"resolved", "dismissed"} and normalized_note is None:
        return {
            "success": False,
            "error": "Resolving or dismissing a finding requires a note with the message to post.",
        }

    updates: dict[str, Any] = {}
    suggestion_dropped = False
    if status is not None:
        updates["status"] = status
    if severity is not None:
        updates["severity"] = severity
    if confidence is not None:
        updates["confidence"] = confidence
    if title is not None:
        normalized_title = normalize_finding_title(title)
        if normalized_title == DEFAULT_FINDING_TITLE:
            return {"success": False, "error": "title must be a non-empty generated headline"}
        updates["title"] = normalized_title
    if description is not None:
        updates["description"] = description
    if suggestion is not None:
        if suggestion == "":
            updates["suggestion"] = None
        else:
            clipped, suggestion_dropped = clip_suggestion(suggestion)
            if not suggestion_dropped:
                updates["suggestion"] = clipped
    if normalized_note is not None:
        updates["last_update_note"] = normalized_note
        if status in {"resolved", "dismissed"}:
            updates["resolution_note"] = normalized_note

    config = get_config()
    configurable = config.get("configurable", {}) if isinstance(config, dict) else {}
    if status == "open":
        try:
            head_sha = await resolve_review_head_sha(get_thread_id_from_runtime(), configurable)
        except ReviewerThreadMissingError as exc:
            return thread_missing_tool_result(exc)
        if head_sha:
            updates["last_confirmed_sha"] = head_sha

    if not updates:
        if suggestion_dropped:
            return {
                "success": False,
                "suggestion_dropped": True,
                "error": (
                    f"Suggestion exceeded the {MAX_SUGGESTION_LINES}-line cap "
                    "and was rejected; no other fields were provided, so "
                    "nothing was updated. Only include `suggestion` for "
                    "small, obvious fixes."
                ),
            }
        return {"success": False, "error": "No fields provided to update"}

    thread_id = get_thread_id_from_runtime()
    try:
        findings = await list_findings(thread_id)
    except ReviewerThreadMissingError as exc:
        return thread_missing_tool_result(exc)
    finding = next((item for item in findings if item.get("id") == finding_id), None)
    if finding is None:
        return {"success": False, "error": f"No finding found with id {finding_id}"}

    delegated_resolution = False
    repo_config = configurable.get("repo") if isinstance(configurable, dict) else None
    pr_number = configurable.get("pr_number") if isinstance(configurable, dict) else None
    can_resolve_github_thread = (
        isinstance(repo_config, dict)
        and bool(repo_config.get("owner"))
        and bool(repo_config.get("name"))
        and isinstance(pr_number, int)
    )
    if (
        status in {"resolved", "dismissed"}
        and can_resolve_github_thread
        and _has_published_github_surface(finding)
    ):
        from .resolve_finding_thread import resolve_finding_thread

        resolve_result = await resolve_finding_thread(
            finding_id, status=status, note=normalized_note
        )
        if not resolve_result.get("success"):
            return {
                "success": False,
                "error": "GitHub review thread resolution failed; finding was left open.",
                "github_resolution": resolve_result,
            }
        delegated_resolution = True
        updates.pop("status", None)
        updates.pop("last_update_note", None)
        updates.pop("resolution_note", None)
        if not updates:
            result: dict[str, Any] = {
                "success": True,
                "finding": resolve_result.get("finding"),
                "github_resolution": resolve_result,
            }
            if suggestion_dropped:
                result["suggestion_dropped"] = True
                result["warning"] = (
                    f"Suggestion exceeded the {MAX_SUGGESTION_LINES}-line cap and was "
                    "rejected — the finding's prior `suggestion` was left unchanged. "
                    "Only include `suggestion` for small, obvious fixes."
                )
            return result

    try:
        updated = await update_finding_fields(thread_id, finding_id, updates)
    except ReviewerThreadMissingError as exc:
        return thread_missing_tool_result(exc)
    if updated is None:
        return {"success": False, "error": f"No finding found with id {finding_id}"}
    if status in {"resolved", "dismissed"} and not delegated_resolution:
        emit_finding_status_outcome(updated, status, configurable=configurable, thread_id=thread_id)
    result = {"success": True, "finding": updated}
    if suggestion_dropped:
        result["suggestion_dropped"] = True
        result["warning"] = (
            f"Suggestion exceeded the {MAX_SUGGESTION_LINES}-line cap and was "
            "rejected — the finding's prior `suggestion` was left unchanged "
            "and other fields were updated normally. Only include "
            "`suggestion` for small, obvious fixes."
        )
    return result
