"""
AWOOOI — Local Code Review Service (Phase 32, ADR-067)
======================================================
使用 qwen2.5-coder:7b 對 Gitea PR diff 進行自動審查
Fallback: Gemini (diff > 50KB 或 Ollama 超時)

觸發: Gitea webhook push event (PR opened/updated)
雙寫: Redis TTL 7d + PostgreSQL pr_reviews 表永久儲存
並發: semaphore 最多 2 個同時審查

2026-04-10 Claude Sonnet 4.6 Asia/Taipei
"""
from __future__ import annotations

import asyncio
from typing import Any

import httpx
import structlog

from src.core.config import get_settings
from src.services.model_registry import get_model
from src.services.ollama_endpoint_resolver import resolve_ollama_order

logger = structlog.get_logger(__name__)
settings = get_settings()

# D1 集中化 2026-04-11: 從 models.json providers.ollama.models.code_review 讀取
_MODEL_OLLAMA = get_model("ollama", "code_review")
_TIMEOUT_OLLAMA = 120.0
_MAX_DIFF_BYTES = 50 * 1024  # 50KB → fallback to Gemini
_SEMAPHORE = asyncio.Semaphore(2)  # 最多 2 個同時審查

# Redis cache TTL: 7 days
_CACHE_TTL = 7 * 86400


class LocalCodeReviewService:
    """PR 自動審查服務"""

    def __init__(self) -> None:
        self._http: httpx.AsyncClient | None = None

    async def _get_http(self) -> httpx.AsyncClient:
        if self._http is None or self._http.is_closed:
            self._http = httpx.AsyncClient(
                timeout=httpx.Timeout(_TIMEOUT_OLLAMA, connect=10.0)
            )
        return self._http

    async def review_pr(
        self,
        pr_id: str,
        repo: str,
        title: str,
        diff: str,
    ) -> dict[str, Any] | None:
        """
        審查 PR diff，回傳審查結果 dict
        {review_text, issues_count, model, provider}
        """
        async with _SEMAPHORE:
            cache_key = f"pr_review:{repo}:{pr_id}"

            # 快取命中
            try:
                from src.core.redis_client import get_redis
                redis = await get_redis()
                if redis:
                    cached = await redis.get(cache_key)
                    if cached:
                        logger.info("pr_review_cache_hit", pr_id=pr_id)
                        import json
                        return json.loads(cached)
            except Exception:
                redis = None

            diff_size = len(diff.encode())
            allow_cloud_fallback = settings.LOCAL_CODE_REVIEW_ALLOW_GEMINI_FALLBACK

            if diff_size > _MAX_DIFF_BYTES and allow_cloud_fallback:
                result = await self._review_with_gemini(pr_id, repo, title, diff)
            else:
                if diff_size > _MAX_DIFF_BYTES:
                    logger.info(
                        "pr_review_large_diff_using_ollama_truncated",
                        pr_id=pr_id,
                        diff_size_bytes=diff_size,
                        cloud_fallback_enabled=allow_cloud_fallback,
                    )
                result = await self._review_with_ollama(pr_id, repo, title, diff)
                if result is None and allow_cloud_fallback:
                    result = await self._review_with_gemini(pr_id, repo, title, diff)

            if result is None:
                result = self._cloud_fallback_disabled_result(pr_id, repo, title)

            result["diff_size_bytes"] = diff_size

            # 寫入快取
            if redis:
                try:
                    import json
                    await redis.set(cache_key, json.dumps(result, ensure_ascii=False), ex=_CACHE_TTL)
                except Exception:
                    pass

            # 寫入 DB
            await self._save_to_db(pr_id, repo, title, diff_size, result)

            return result

    async def _review_with_ollama(
        self, pr_id: str, repo: str, title: str, diff: str
    ) -> dict[str, Any] | None:
        prompt = (
            f"你是資深程式審查員，請用繁體中文審查以下 Pull Request。\n"
            f"PR: {repo}#{pr_id} — {title}\n\n"
            "請找出：1) 潛在 Bug 或邏輯錯誤 2) 安全問題 3) 效能問題 4) 代碼風格問題\n"
            "格式：每個問題獨立一行，以「⚠️」開頭。如果沒有問題，說「✅ 程式碼品質良好」\n\n"
            f"=== Diff ===\n{diff[:40000]}\n=== 結束 ==="
        )
        http = await self._get_http()
        for endpoint in resolve_ollama_order("code_review"):
            if not endpoint.url:
                continue
            try:
                resp = await http.post(
                    f"{endpoint.url}/api/generate",
                    json={
                        "model": _MODEL_OLLAMA,
                        "prompt": prompt,
                        "stream": False,
                        "options": {"num_predict": 1024, "temperature": 0.1},
                    },
                )
                if resp.status_code == 200:
                    text = resp.json().get("response", "").strip()
                    issues = text.count("⚠️")
                    logger.info(
                        "pr_review_ollama_done",
                        pr_id=pr_id,
                        issues=issues,
                        provider=endpoint.provider_name,
                    )
                    return {
                        "review_text": text,
                        "issues_count": issues,
                        "model": _MODEL_OLLAMA,
                        "provider": endpoint.provider_name,
                    }
                logger.warning(
                    "pr_review_ollama_http_error",
                    pr_id=pr_id,
                    provider=endpoint.provider_name,
                    status=resp.status_code,
                )
            except httpx.TimeoutException:
                logger.warning(
                    "pr_review_ollama_timeout",
                    pr_id=pr_id,
                    provider=endpoint.provider_name,
                )
            except Exception as e:
                logger.error(
                    "pr_review_ollama_failed",
                    pr_id=pr_id,
                    provider=endpoint.provider_name,
                    error=str(e),
                )
        return None

    async def _review_with_gemini(
        self, pr_id: str, repo: str, title: str, diff: str
    ) -> dict[str, Any] | None:
        if not settings.LOCAL_CODE_REVIEW_ALLOW_GEMINI_FALLBACK:
            logger.warning(
                "pr_review_gemini_fallback_disabled",
                pr_id=pr_id,
                repo=repo,
            )
            return None

        try:
            from src.services.openclaw import get_openclaw
            openclaw = get_openclaw()
            prompt = (
                f"PR Code Review: {repo}#{pr_id} — {title}\n"
                "繁體中文，找出 Bug/安全/效能/風格問題，每問題以⚠️開頭\n\n"
                f"Diff:\n{diff[:60000]}"
            )
            # 直接呼叫 Gemini
            result = await openclaw._call_gemini(prompt)
            if result and result[0]:
                text = result[0]
                issues = text.count("⚠️")
                return {"review_text": text, "issues_count": issues, "model": "gemini", "provider": "gemini"}
        except Exception as e:
            logger.error("pr_review_gemini_failed", pr_id=pr_id, error=str(e))
        return None

    def _cloud_fallback_disabled_result(
        self,
        pr_id: str,
        repo: str,
        title: str,
    ) -> dict[str, Any]:
        logger.warning(
            "pr_review_cloud_fallback_skipped",
            pr_id=pr_id,
            repo=repo,
            title=title,
            reason="LOCAL_CODE_REVIEW_ALLOW_GEMINI_FALLBACK=false",
        )
        return {
            "review_text": (
                "⚠️ Code Review：本地 Ollama 審查未完成，"
                "已依成本策略跳過 Gemini fallback。"
            ),
            "issues_count": 1,
            "model": _MODEL_OLLAMA,
            "provider": "ollama_unavailable",
            "cloud_fallback_skipped": True,
        }

    async def _save_to_db(
        self, pr_id: str, repo: str, title: str, diff_size: int, result: dict
    ) -> None:
        try:
            from sqlalchemy import text

            from src.db.base import get_db_context
            async with get_db_context() as db:
                await db.execute(
                    text("""
                        INSERT INTO pr_reviews
                            (pr_id, repo, title, diff_size_bytes, model, provider, review_text, issues_count)
                        VALUES
                            (:pr_id, :repo, :title, :diff_size, :model, :provider, :review_text, :issues_count)
                    """),
                    {
                        "pr_id": pr_id, "repo": repo, "title": title,
                        "diff_size": diff_size,
                        "model": result.get("model", "unknown"),
                        "provider": result.get("provider", "unknown"),
                        "review_text": result.get("review_text", ""),
                        "issues_count": result.get("issues_count", 0),
                    },
                )
        except Exception as e:
            logger.warning("pr_review_db_save_failed", error=str(e))

    async def review_push(
        self,
        repo_name: str,
        branch: str,
        commit_summary: str,
        files_summary: str,
    ) -> dict[str, Any] | None:
        """
        審查 push event（非 PR），用 qwen2.5-coder:7b 快速掃描
        2026-04-10 Claude Sonnet 4.6: Phase 32 ADR-067 — gitea_webhook_service 呼叫
        """
        async with _SEMAPHORE:
            prompt = (
                f"你是資深程式審查員，請用繁體中文快速審查以下 Git Push。\n"
                f"Repository: {repo_name}  Branch: {branch}\n\n"
                f"=== Commits ===\n{commit_summary}\n\n"
                f"=== Changed Files ===\n{files_summary}\n\n"
                "請簡要說明：1) 有無明顯安全風險 2) 有無破壞性變更 3) 整體品質評估\n"
                "格式：每個問題以「⚠️」開頭，如無問題說「✅ Push 品質正常」\n"
                "5 行以內，繁體中文。"
            )
            http = await self._get_http()
            for endpoint in resolve_ollama_order("code_review"):
                if not endpoint.url:
                    continue
                try:
                    resp = await http.post(
                        f"{endpoint.url}/api/generate",
                        json={
                            "model": _MODEL_OLLAMA,
                            "prompt": prompt,
                            "stream": False,
                            "options": {"num_predict": 512, "temperature": 0.1},
                        },
                    )
                    if resp.status_code == 200:
                        text = resp.json().get("response", "").strip()
                        issues = text.count("⚠️")
                        logger.info(
                            "push_review_ollama_done",
                            repo=repo_name,
                            branch=branch,
                            issues=issues,
                            provider=endpoint.provider_name,
                        )
                        return {
                            "review_text": text,
                            "issues_count": issues,
                            "model": _MODEL_OLLAMA,
                            "provider": endpoint.provider_name,
                        }
                    logger.warning(
                        "push_review_ollama_http_error",
                        repo=repo_name,
                        provider=endpoint.provider_name,
                        status=resp.status_code,
                    )
                except httpx.TimeoutException:
                    logger.warning(
                        "push_review_ollama_timeout",
                        repo=repo_name,
                        provider=endpoint.provider_name,
                    )
                except Exception as e:
                    logger.error(
                        "push_review_ollama_failed",
                        repo=repo_name,
                        provider=endpoint.provider_name,
                        error=str(e),
                    )
            return None

    async def close(self) -> None:
        if self._http and not self._http.is_closed:
            await self._http.aclose()


_instance: LocalCodeReviewService | None = None


def get_local_code_review_service() -> LocalCodeReviewService:
    global _instance
    if _instance is None:
        _instance = LocalCodeReviewService()
    return _instance


def set_local_code_review_service(svc: LocalCodeReviewService) -> None:
    global _instance
    _instance = svc
