# -*- coding: utf-8 -*-
"""
Nom du fichier : milestones.py
Chemin : /var/www/html/gitlab-bridge/app/api/v1/milestones.py
Description : Endpoints API liés aux milestones GitLab compatibles OpenWebUI.
Auteur : Sylvain SCATTOLINI
Date de création / modification : 2026-04-02
Version : 1.1
"""

from __future__ import annotations

import json
import logging
from typing import Any

from fastapi import APIRouter, HTTPException

from app.api.v1.issues import (
    _build_candidate_projects,
    _build_issue_candidates,
    _get_issue_by_iid,
    _get_project_by_path,
    _gitlab_request,
    _normalize_text,
    _score_text,
    _search_best_project,
    _search_open_issues_in_project,
)
from app.schemas.milestones import (
    MilestoneAssistantAssignIssueRequest,
    MilestoneAssistantAssignIssueResponse,
    MilestoneAssistantCreateRequest,
    MilestoneAssistantCreateResponse,
    MilestoneAssistantListRequest,
    MilestoneAssistantListResponse,
    MilestoneCandidateItem,
    MilestoneItem,
    MilestoneAssistantListIssuesRequest,
    MilestoneAssistantListIssuesResponse,
    MilestoneAssistantProgressRequest,
    MilestoneAssistantProgressResponse,
)

router = APIRouter(prefix="/api/v1/milestones", tags=["milestones"])

logger = logging.getLogger(__name__)


def _milestone_to_item(milestone: dict[str, Any]) -> MilestoneItem:
    return MilestoneItem(
        id=int(milestone["id"]),
        iid=int(milestone["iid"]) if milestone.get("iid") is not None else None,
        title=str(milestone.get("title", "")),
        description=str(milestone.get("description", "")) if milestone.get("description") is not None else None,
        state=str(milestone.get("state", "")),
        due_date=str(milestone.get("due_date")) if milestone.get("due_date") is not None else None,
        web_url=str(milestone.get("web_url")) if milestone.get("web_url") is not None else None,
    )


def _list_project_milestones(project_id: int, state: str, limit: int) -> list[MilestoneItem]:
    query_state = "active" if state == "active" else "closed" if state == "closed" else None

    milestones = _gitlab_request(
        "GET",
        f"/projects/{project_id}/milestones",
        query_params={
            "state": query_state,
            "per_page": min(limit, 100),
            "page": 1,
        },
    )

    if not isinstance(milestones, list):
        return []

    return [_milestone_to_item(m) for m in milestones if isinstance(m, dict)]


def _create_project_milestone(
    project_id: int,
    title: str,
    description: str,
    due_date: str,
) -> MilestoneItem:
    payload: dict[str, Any] = {
        "title": title.strip(),
    }

    if description.strip():
        payload["description"] = description.strip()

    if due_date.strip():
        payload["due_date"] = due_date.strip()

    milestone = _gitlab_request(
        "POST",
        f"/projects/{project_id}/milestones",
        payload=payload,
    )

    if not isinstance(milestone, dict):
        raise HTTPException(status_code=502, detail="Réponse GitLab invalide lors de la création de la milestone.")

    return _milestone_to_item(milestone)


def _search_milestones_in_project(project_id: int, milestone_hint: str, limit: int) -> list[tuple[int, dict[str, Any]]]:
    milestones = _gitlab_request(
        "GET",
        f"/projects/{project_id}/milestones",
        query_params={
            "state": "active",
            "per_page": min(limit, 100),
            "page": 1,
        },
    )

    if not isinstance(milestones, list):
        return []

    candidates: list[tuple[int, dict[str, Any]]] = []

    for milestone in milestones:
        if not isinstance(milestone, dict):
            continue

        title = str(milestone.get("title", ""))
        score = _score_text(milestone_hint, title)

        if _normalize_text(milestone_hint) in _normalize_text(title):
            score = max(score, 95)

        if score < 40:
            continue

        candidates.append((score, milestone))

    candidates.sort(reverse=True, key=lambda item: item[0])
    return candidates[:limit]


def _build_milestone_candidates(candidates: list[tuple[int, dict[str, Any]]]) -> list[MilestoneCandidateItem]:
    results: list[MilestoneCandidateItem] = []

    for score, milestone in candidates[:5]:
        results.append(
            MilestoneCandidateItem(
                id=int(milestone["id"]),
                iid=int(milestone["iid"]) if milestone.get("iid") is not None else None,
                title=str(milestone.get("title", "")),
                state=str(milestone.get("state", "")),
                due_date=str(milestone.get("due_date")) if milestone.get("due_date") is not None else None,
                web_url=str(milestone.get("web_url")) if milestone.get("web_url") is not None else None,
                score=score,
            )
        )

    return results


def _assign_issue_to_milestone(project_id: int, issue_iid: int, milestone_id: int) -> dict[str, Any]:
    issue = _gitlab_request(
        "PUT",
        f"/projects/{project_id}/issues/{issue_iid}",
        payload={"milestone_id": milestone_id},
    )

    if not isinstance(issue, dict):
        raise HTTPException(status_code=502, detail="Réponse GitLab invalide lors de l'affectation de la milestone.")

    return issue


def _get_milestone_by_id(project_id: int, milestone_id: int) -> dict[str, Any]:
    milestone = _gitlab_request(
        "GET",
        f"/projects/{project_id}/milestones/{milestone_id}",
    )

    if not isinstance(milestone, dict) or "id" not in milestone:
        raise HTTPException(status_code=404, detail="Milestone introuvable")

    return milestone


def _list_issues_by_milestone(project_id: int, milestone_title: str, state: str, limit: int) -> list[dict[str, Any]]:
    query_state = None if state == "all" else state

    issues = _gitlab_request(
        "GET",
        f"/projects/{project_id}/issues",
        query_params={
            "milestone": milestone_title,
            "state": query_state,
            "per_page": min(limit, 100),
            "page": 1,
            "order_by": "created_at",
            "sort": "desc",
        },
    )

    if not isinstance(issues, list):
        return []

    results: list[dict[str, Any]] = []

    for issue in issues:
        if not isinstance(issue, dict):
            continue

        results.append(
            {
                "id": int(issue["id"]),
                "iid": int(issue["iid"]),
                "title": str(issue.get("title", "")),
                "state": str(issue.get("state", "")),
                "web_url": str(issue.get("web_url", "")),
            }
        )

    return results

def _list_all_issues_by_milestone(project_id: int, milestone_title: str, limit: int) -> list[dict[str, Any]]:
    issues = _gitlab_request(
        "GET",
        f"/projects/{project_id}/issues",
        query_params={
            "milestone": milestone_title,
            "state": None,
            "per_page": min(limit, 100),
            "page": 1,
            "order_by": "created_at",
            "sort": "desc",
        },
    )

    if not isinstance(issues, list):
        return []

    return [issue for issue in issues if isinstance(issue, dict)]


def _compute_milestone_progress(project_id: int, milestone_title: str, limit: int) -> tuple[int, int, int, int]:
    issues = _list_all_issues_by_milestone(project_id, milestone_title, limit)

    total = len(issues)
    opened = 0
    closed = 0

    for issue in issues:
        state = str(issue.get("state", "")).lower()
        if state == "closed":
            closed += 1
        else:
            opened += 1

    progress_percent = int((closed / total) * 100) if total > 0 else 0

    return total, opened, closed, progress_percent

# ------------------------------------------------------
# Endpoints API
# ------------------------------------------------------

@router.post(
    "/assistant-list",
    summary="Assistant liste des milestones",
    response_model=MilestoneAssistantListResponse,
)
def assistant_list_milestones(
    payload: MilestoneAssistantListRequest,
) -> MilestoneAssistantListResponse:
    try:
        logger.info(
            "Payload reçu sur /api/v1/milestones/assistant-list : %s",
            json.dumps(payload.model_dump(), ensure_ascii=False),
        )

        safe_limit = int(payload.limit)
        safe_state = payload.state.strip().lower()

        if payload.confirm:
            if not payload.resolved_project_path.strip():
                raise HTTPException(status_code=400, detail="Le champ resolved_project_path est obligatoire quand confirm=true.")

            project = _get_project_by_path(payload.resolved_project_path.strip())
            milestones = _list_project_milestones(int(project["id"]), safe_state, safe_limit)

            return MilestoneAssistantListResponse(
                success=True,
                status="resolved",
                message=f"Milestones récupérées pour {project['path_with_namespace']}",
                project={
                    "id": int(project["id"]),
                    "name": str(project.get("name", "")),
                    "path_with_namespace": str(project.get("path_with_namespace", "")),
                },
                milestones=milestones,
                candidate_projects=[],
            )

        candidates = _search_best_project(payload.project_hint, payload.root_hint)

        if not candidates:
            return MilestoneAssistantListResponse(
                success=True,
                status="not_found",
                message=f"Je ne trouve pas de projet correspondant à '{payload.project_hint}'.",
                project=None,
                milestones=[],
                candidate_projects=[],
            )

        if len(candidates) > 1 and candidates[1][0] >= candidates[0][0] - 10:
            return MilestoneAssistantListResponse(
                success=True,
                status="clarification_needed",
                message="J’ai trouvé plusieurs projets possibles. Merci de préciser lequel utiliser.",
                project=None,
                milestones=[],
                candidate_projects=_build_candidate_projects(candidates),
            )

        project = candidates[0][1]
        milestones = _list_project_milestones(int(project["id"]), safe_state, safe_limit)

        return MilestoneAssistantListResponse(
            success=True,
            status="resolved",
            message=f"Milestones récupérées pour {project['path_with_namespace']}",
            project={
                "id": int(project["id"]),
                "name": str(project.get("name", "")),
                "path_with_namespace": str(project.get("path_with_namespace", "")),
            },
            milestones=milestones,
            candidate_projects=_build_candidate_projects(candidates[:1]),
        )

    except HTTPException:
        raise
    except Exception as exc:
        logger.exception("Erreur inattendue dans assistant_list_milestones : %s", str(exc))
        raise HTTPException(status_code=500, detail="Erreur interne lors de la récupération assistée des milestones.") from exc


@router.post(
    "/assistant-create",
    summary="Assistant création de milestone",
    response_model=MilestoneAssistantCreateResponse,
)
def assistant_create_milestone(
    payload: MilestoneAssistantCreateRequest,
) -> MilestoneAssistantCreateResponse:
    try:
        logger.info(
            "Payload reçu sur /api/v1/milestones/assistant-create : %s",
            json.dumps(payload.model_dump(), ensure_ascii=False),
        )

        if payload.confirm:
            if not payload.resolved_project_path.strip():
                raise HTTPException(status_code=400, detail="Le champ resolved_project_path est obligatoire quand confirm=true.")

            project = _get_project_by_path(payload.resolved_project_path.strip())
            milestone = _create_project_milestone(
                int(project["id"]),
                payload.title,
                payload.description,
                payload.due_date,
            )

            return MilestoneAssistantCreateResponse(
                success=True,
                status="created",
                message=f"Milestone créée : {milestone.title}",
                project={
                    "id": int(project["id"]),
                    "name": str(project.get("name", "")),
                    "path_with_namespace": str(project.get("path_with_namespace", "")),
                },
                milestone=milestone,
                candidate_projects=[],
            )

        candidates = _search_best_project(payload.project_hint, payload.root_hint)

        if not candidates:
            return MilestoneAssistantCreateResponse(
                success=True,
                status="not_found",
                message=f"Je ne trouve pas de projet correspondant à '{payload.project_hint}'.",
                project=None,
                milestone=None,
                candidate_projects=[],
            )

        if len(candidates) > 1 and candidates[1][0] >= candidates[0][0] - 10:
            return MilestoneAssistantCreateResponse(
                success=True,
                status="clarification_needed",
                message="J’ai trouvé plusieurs projets possibles. Merci de préciser lequel utiliser.",
                project=None,
                milestone=None,
                candidate_projects=_build_candidate_projects(candidates),
            )

        project = candidates[0][1]
        milestone = _create_project_milestone(
            int(project["id"]),
            payload.title,
            payload.description,
            payload.due_date,
        )

        return MilestoneAssistantCreateResponse(
            success=True,
            status="created",
            message=f"Milestone créée : {milestone.title}",
            project={
                "id": int(project["id"]),
                "name": str(project.get("name", "")),
                "path_with_namespace": str(project.get("path_with_namespace", "")),
            },
            milestone=milestone,
            candidate_projects=_build_candidate_projects(candidates[:1]),
        )

    except HTTPException:
        raise
    except Exception as exc:
        logger.exception("Erreur inattendue dans assistant_create_milestone : %s", str(exc))
        raise HTTPException(status_code=500, detail="Erreur interne lors de la création assistée de milestone.") from exc


@router.post(
    "/assistant-assign-issue",
    summary="Assistant affectation d'une issue à une milestone",
    response_model=MilestoneAssistantAssignIssueResponse,
)
def assistant_assign_issue_to_milestone(
    payload: MilestoneAssistantAssignIssueRequest,
) -> MilestoneAssistantAssignIssueResponse:
    try:
        logger.info(
            "Payload reçu sur /api/v1/milestones/assistant-assign-issue : %s",
            json.dumps(payload.model_dump(), ensure_ascii=False),
        )

        safe_limit = int(payload.limit)

        if payload.confirm:
            if not payload.resolved_project_path.strip():
                raise HTTPException(status_code=400, detail="Le champ resolved_project_path est obligatoire quand confirm=true.")
            if payload.resolved_issue_iid <= 0:
                raise HTTPException(status_code=400, detail="Le champ resolved_issue_iid est obligatoire quand confirm=true.")
            if payload.resolved_milestone_id <= 0:
                raise HTTPException(status_code=400, detail="Le champ resolved_milestone_id est obligatoire quand confirm=true.")

            project = _get_project_by_path(payload.resolved_project_path.strip())
            updated_issue = _assign_issue_to_milestone(
                int(project["id"]),
                int(payload.resolved_issue_iid),
                int(payload.resolved_milestone_id),
            )

            milestone_data = updated_issue.get("milestone") or {}

            return MilestoneAssistantAssignIssueResponse(
                success=True,
                status="assigned",
                message=f"Issue affectée à la milestone : {updated_issue.get('title', '')}",
                project={
                    "id": int(project["id"]),
                    "name": str(project.get("name", "")),
                    "path_with_namespace": str(project.get("path_with_namespace", "")),
                },
                issue={
                    "id": int(updated_issue["id"]),
                    "iid": int(updated_issue["iid"]),
                    "title": str(updated_issue.get("title", "")),
                    "state": str(updated_issue.get("state", "")),
                    "web_url": str(updated_issue.get("web_url", "")),
                },
                milestone=_milestone_to_item(milestone_data) if isinstance(milestone_data, dict) and milestone_data else None,
                candidate_projects=[],
                candidate_issues=[],
                candidate_milestones=[],
            )

        candidates = _search_best_project(payload.project_hint, payload.root_hint)

        if not candidates:
            return MilestoneAssistantAssignIssueResponse(
                success=True,
                status="project_not_found",
                message="Je ne trouve pas le projet correspondant.",
                project=None,
                issue=None,
                milestone=None,
                candidate_projects=[],
                candidate_issues=[],
                candidate_milestones=[],
            )

        if len(candidates) > 1 and candidates[1][0] >= candidates[0][0] - 10:
            return MilestoneAssistantAssignIssueResponse(
                success=True,
                status="project_clarification_needed",
                message="J’ai trouvé plusieurs projets possibles. Merci de préciser lequel utiliser.",
                project=None,
                issue=None,
                milestone=None,
                candidate_projects=_build_candidate_projects(candidates),
                candidate_issues=[],
                candidate_milestones=[],
            )

        project = candidates[0][1]
        project_id = int(project["id"])

        issue_candidates = _search_open_issues_in_project(project_id, payload.issue_hint, safe_limit)
        if not issue_candidates:
            return MilestoneAssistantAssignIssueResponse(
                success=True,
                status="issue_not_found",
                message=f"Je ne trouve pas d'issue ouverte correspondant à '{payload.issue_hint}'.",
                project={
                    "id": project_id,
                    "name": str(project.get("name", "")),
                    "path_with_namespace": str(project.get("path_with_namespace", "")),
                },
                issue=None,
                milestone=None,
                candidate_projects=[],
                candidate_issues=[],
                candidate_milestones=[],
            )

        if len(issue_candidates) > 1 and issue_candidates[1][0] >= issue_candidates[0][0] - 10:
            return MilestoneAssistantAssignIssueResponse(
                success=True,
                status="issue_clarification_needed",
                message="J’ai trouvé plusieurs issues possibles. Merci de préciser laquelle utiliser.",
                project={
                    "id": project_id,
                    "name": str(project.get("name", "")),
                    "path_with_namespace": str(project.get("path_with_namespace", "")),
                },
                issue=None,
                milestone=None,
                candidate_projects=[],
                candidate_issues=_build_issue_candidates(issue_candidates),
                candidate_milestones=[],
            )

        milestone_candidates = _search_milestones_in_project(project_id, payload.milestone_hint, safe_limit)
        if not milestone_candidates:
            return MilestoneAssistantAssignIssueResponse(
                success=True,
                status="milestone_not_found",
                message=f"Je ne trouve pas de milestone correspondant à '{payload.milestone_hint}'.",
                project={
                    "id": project_id,
                    "name": str(project.get("name", "")),
                    "path_with_namespace": str(project.get("path_with_namespace", "")),
                },
                issue=None,
                milestone=None,
                candidate_projects=[],
                candidate_issues=[],
                candidate_milestones=[],
            )

        if len(milestone_candidates) > 1 and milestone_candidates[1][0] >= milestone_candidates[0][0] - 10:
            return MilestoneAssistantAssignIssueResponse(
                success=True,
                status="milestone_clarification_needed",
                message="J’ai trouvé plusieurs milestones possibles. Merci de préciser laquelle utiliser.",
                project={
                    "id": project_id,
                    "name": str(project.get("name", "")),
                    "path_with_namespace": str(project.get("path_with_namespace", "")),
                },
                issue=None,
                milestone=None,
                candidate_projects=[],
                candidate_issues=[],
                candidate_milestones=_build_milestone_candidates(milestone_candidates),
            )

        issue_iid = int(issue_candidates[0][1]["iid"])
        milestone_id = int(milestone_candidates[0][1]["id"])

        updated_issue = _assign_issue_to_milestone(project_id, issue_iid, milestone_id)
        milestone_data = updated_issue.get("milestone") or {}

        return MilestoneAssistantAssignIssueResponse(
            success=True,
            status="assigned",
            message=f"Issue affectée à la milestone : {updated_issue.get('title', '')}",
            project={
                "id": project_id,
                "name": str(project.get("name", "")),
                "path_with_namespace": str(project.get("path_with_namespace", "")),
            },
            issue={
                "id": int(updated_issue["id"]),
                "iid": int(updated_issue["iid"]),
                "title": str(updated_issue.get("title", "")),
                "state": str(updated_issue.get("state", "")),
                "web_url": str(updated_issue.get("web_url", "")),
            },
            milestone=_milestone_to_item(milestone_data) if isinstance(milestone_data, dict) and milestone_data else None,
            candidate_projects=[],
            candidate_issues=[],
            candidate_milestones=[],
        )

    except HTTPException:
        raise
    except Exception as exc:
        logger.exception("Erreur inattendue dans assistant_assign_issue_to_milestone : %s", str(exc))
        raise HTTPException(status_code=500, detail="Erreur interne lors de l'affectation assistée d'issue à une milestone.") from exc

@router.post(
    "/assistant-list-issues",
    summary="Assistant liste des issues d'une milestone",
    response_model=MilestoneAssistantListIssuesResponse,
)
def assistant_list_issues_by_milestone(
    payload: MilestoneAssistantListIssuesRequest,
) -> MilestoneAssistantListIssuesResponse:
    try:
        logger.info(
            "Payload reçu sur /api/v1/milestones/assistant-list-issues : %s",
            json.dumps(payload.model_dump(), ensure_ascii=False),
        )

        safe_limit = int(payload.limit)
        safe_state = payload.state.strip().lower()

        if payload.confirm:
            if not payload.resolved_project_path.strip():
                raise HTTPException(
                    status_code=400,
                    detail="Le champ resolved_project_path est obligatoire quand confirm=true.",
                )

            if payload.resolved_milestone_id <= 0:
                raise HTTPException(
                    status_code=400,
                    detail="Le champ resolved_milestone_id est obligatoire quand confirm=true.",
                )

            project = _get_project_by_path(payload.resolved_project_path.strip())
            project_id = int(project["id"])

            milestone = _get_milestone_by_id(project_id, int(payload.resolved_milestone_id))
            milestone_item = _milestone_to_item(milestone)
            issues = _list_issues_by_milestone(project_id, milestone_item.title, safe_state, safe_limit)

            return MilestoneAssistantListIssuesResponse(
                success=True,
                status="resolved",
                message=f"Issues récupérées pour la milestone {milestone_item.title}",
                project={
                    "id": project_id,
                    "name": str(project.get("name", "")),
                    "path_with_namespace": str(project.get("path_with_namespace", "")),
                },
                milestone=milestone_item,
                issues=issues,
                candidate_projects=[],
                candidate_milestones=[],
            )

        candidates = _search_best_project(payload.project_hint, payload.root_hint)

        if not candidates:
            return MilestoneAssistantListIssuesResponse(
                success=True,
                status="project_not_found",
                message="Je ne trouve pas le projet correspondant.",
                project=None,
                milestone=None,
                issues=[],
                candidate_projects=[],
                candidate_milestones=[],
            )

        if len(candidates) > 1 and candidates[1][0] >= candidates[0][0] - 10:
            return MilestoneAssistantListIssuesResponse(
                success=True,
                status="project_clarification_needed",
                message="J’ai trouvé plusieurs projets possibles. Merci de préciser lequel utiliser.",
                project=None,
                milestone=None,
                issues=[],
                candidate_projects=_build_candidate_projects(candidates),
                candidate_milestones=[],
            )

        project = candidates[0][1]
        project_id = int(project["id"])

        milestone_candidates = _search_milestones_in_project(project_id, payload.milestone_hint, safe_limit)

        if not milestone_candidates:
            return MilestoneAssistantListIssuesResponse(
                success=True,
                status="milestone_not_found",
                message=f"Je ne trouve pas de milestone correspondant à '{payload.milestone_hint}'.",
                project={
                    "id": project_id,
                    "name": str(project.get("name", "")),
                    "path_with_namespace": str(project.get("path_with_namespace", "")),
                },
                milestone=None,
                issues=[],
                candidate_projects=[],
                candidate_milestones=[],
            )

        if len(milestone_candidates) > 1 and milestone_candidates[1][0] >= milestone_candidates[0][0] - 10:
            return MilestoneAssistantListIssuesResponse(
                success=True,
                status="milestone_clarification_needed",
                message="J’ai trouvé plusieurs milestones possibles. Merci de préciser laquelle utiliser.",
                project={
                    "id": project_id,
                    "name": str(project.get("name", "")),
                    "path_with_namespace": str(project.get("path_with_namespace", "")),
                },
                milestone=None,
                issues=[],
                candidate_projects=[],
                candidate_milestones=_build_milestone_candidates(milestone_candidates),
            )

        milestone = milestone_candidates[0][1]
        milestone_item = _milestone_to_item(milestone)
        issues = _list_issues_by_milestone(project_id, milestone_item.title, safe_state, safe_limit)

        return MilestoneAssistantListIssuesResponse(
            success=True,
            status="resolved",
            message=f"Issues récupérées pour la milestone {milestone_item.title}",
            project={
                "id": project_id,
                "name": str(project.get("name", "")),
                "path_with_namespace": str(project.get("path_with_namespace", "")),
            },
            milestone=milestone_item,
            issues=issues,
            candidate_projects=[],
            candidate_milestones=[],
        )

    except HTTPException:
        raise
    except Exception as exc:
        logger.exception("Erreur inattendue dans assistant_list_issues_by_milestone : %s", str(exc))
        raise HTTPException(
            status_code=500,
            detail="Erreur interne lors de la récupération assistée des issues de milestone.",
        ) from exc

@router.post(
    "/assistant-progress",
    summary="Assistant progression d'une milestone",
    response_model=MilestoneAssistantProgressResponse,
)
def assistant_progress_by_milestone(
    payload: MilestoneAssistantProgressRequest,
) -> MilestoneAssistantProgressResponse:
    try:
        logger.info(
            "Payload reçu sur /api/v1/milestones/assistant-progress : %s",
            json.dumps(payload.model_dump(), ensure_ascii=False),
        )

        safe_limit = int(payload.limit)

        if payload.confirm:
            if not payload.resolved_project_path.strip():
                raise HTTPException(
                    status_code=400,
                    detail="Le champ resolved_project_path est obligatoire quand confirm=true.",
                )

            if payload.resolved_milestone_id <= 0:
                raise HTTPException(
                    status_code=400,
                    detail="Le champ resolved_milestone_id est obligatoire quand confirm=true.",
                )

            project = _get_project_by_path(payload.resolved_project_path.strip())
            project_id = int(project["id"])

            milestone = _get_milestone_by_id(project_id, int(payload.resolved_milestone_id))
            milestone_item = _milestone_to_item(milestone)

            total, opened, closed, progress_percent = _compute_milestone_progress(
                project_id,
                milestone_item.title,
                safe_limit,
            )

            return MilestoneAssistantProgressResponse(
                success=True,
                status="resolved",
                message=f"Progression calculée pour la milestone {milestone_item.title}",
                project={
                    "id": project_id,
                    "name": str(project.get("name", "")),
                    "path_with_namespace": str(project.get("path_with_namespace", "")),
                },
                milestone=milestone_item,
                total=total,
                opened=opened,
                closed=closed,
                progress_percent=progress_percent,
                candidate_projects=[],
                candidate_milestones=[],
            )

        candidates = _search_best_project(payload.project_hint, payload.root_hint)

        if not candidates:
            return MilestoneAssistantProgressResponse(
                success=True,
                status="project_not_found",
                message="Je ne trouve pas le projet correspondant.",
                project=None,
                milestone=None,
                total=0,
                opened=0,
                closed=0,
                progress_percent=0,
                candidate_projects=[],
                candidate_milestones=[],
            )

        if len(candidates) > 1 and candidates[1][0] >= candidates[0][0] - 10:
            return MilestoneAssistantProgressResponse(
                success=True,
                status="project_clarification_needed",
                message="J’ai trouvé plusieurs projets possibles. Merci de préciser lequel utiliser.",
                project=None,
                milestone=None,
                total=0,
                opened=0,
                closed=0,
                progress_percent=0,
                candidate_projects=_build_candidate_projects(candidates),
                candidate_milestones=[],
            )

        project = candidates[0][1]
        project_id = int(project["id"])

        milestone_candidates = _search_milestones_in_project(project_id, payload.milestone_hint, safe_limit)

        if not milestone_candidates:
            return MilestoneAssistantProgressResponse(
                success=True,
                status="milestone_not_found",
                message=f"Je ne trouve pas de milestone correspondant à '{payload.milestone_hint}'.",
                project={
                    "id": project_id,
                    "name": str(project.get("name", "")),
                    "path_with_namespace": str(project.get("path_with_namespace", "")),
                },
                milestone=None,
                total=0,
                opened=0,
                closed=0,
                progress_percent=0,
                candidate_projects=[],
                candidate_milestones=[],
            )

        if len(milestone_candidates) > 1 and milestone_candidates[1][0] >= milestone_candidates[0][0] - 10:
            return MilestoneAssistantProgressResponse(
                success=True,
                status="milestone_clarification_needed",
                message="J’ai trouvé plusieurs milestones possibles. Merci de préciser laquelle utiliser.",
                project={
                    "id": project_id,
                    "name": str(project.get("name", "")),
                    "path_with_namespace": str(project.get("path_with_namespace", "")),
                },
                milestone=None,
                total=0,
                opened=0,
                closed=0,
                progress_percent=0,
                candidate_projects=[],
                candidate_milestones=_build_milestone_candidates(milestone_candidates),
            )

        milestone = milestone_candidates[0][1]
        milestone_item = _milestone_to_item(milestone)

        total, opened, closed, progress_percent = _compute_milestone_progress(
            project_id,
            milestone_item.title,
            safe_limit,
        )

        return MilestoneAssistantProgressResponse(
            success=True,
            status="resolved",
            message=f"Progression calculée pour la milestone {milestone_item.title}",
            project={
                "id": project_id,
                "name": str(project.get("name", "")),
                "path_with_namespace": str(project.get("path_with_namespace", "")),
            },
            milestone=milestone_item,
            total=total,
            opened=opened,
            closed=closed,
            progress_percent=progress_percent,
            candidate_projects=[],
            candidate_milestones=[],
        )

    except HTTPException:
        raise
    except Exception as exc:
        logger.exception("Erreur inattendue dans assistant_progress_by_milestone : %s", str(exc))
        raise HTTPException(
            status_code=500,
            detail="Erreur interne lors du calcul assisté de progression de milestone.",
        ) from exc