"""Обработка webhook от репозиториев (Gitea, Bitbucket) для привязки коммитов к историям."""

from __future__ import annotations

import re
from typing import Protocol


_STORY_ID_RE = re.compile(r"\b[A-Z][A-Z0-9_]*-\d+\b")


class StoryCommitWriter(Protocol):
    def record_story_commit(
        self,
        *,
        story_id: str,
        project_id: str,
        title: str,
        commit_sha: str | None,
        branch: str | None,
        changed_files: list[str],
        summary: str,
        actor: str | None,
    ) -> None: ...


class RepoCacheWriter(Protocol):
    def record_repo_cache(
        self,
        *,
        project_id: str,
        commit_sha: str | None,
        changed_files: list[str],
        summary: str,
    ) -> None: ...


class RepoWebhookService:
    def __init__(self, story_writer: StoryCommitWriter, cache_writer: RepoCacheWriter | None = None) -> None:
        self._story_writer = story_writer
        self._cache_writer = cache_writer

    def process(self, *, payload: dict, provider: str | None = None, headers: dict | None = None) -> dict:
        resolved_provider = self._resolve_provider(provider=provider, payload=payload, headers=headers or {})
        normalized = self._normalize(provider=resolved_provider, payload=payload)
        if not normalized:
            return {"accepted": False, "reason": "unsupported_or_invalid_payload"}

        cache_recorded = False
        if self._cache_writer is not None:
            self._cache_writer.record_repo_cache(
                project_id=normalized["project_id"],
                commit_sha=normalized["commit_sha"],
                changed_files=normalized["changed_files"],
                summary=normalized["summary"],
            )
            cache_recorded = True

        story_id = self._extract_story_id(normalized["messages"])
        if not story_id:
            return {
                "accepted": True,
                "indexed": False,
                "story_bound": False,
                "cache_recorded": cache_recorded,
                "reason": "story_id_not_found",
            }

        self._story_writer.record_story_commit(
            story_id=story_id,
            project_id=normalized["project_id"],
            title=f"Story {story_id}",
            commit_sha=normalized["commit_sha"],
            branch=normalized["branch"],
            changed_files=normalized["changed_files"],
            summary=normalized["summary"],
            actor=normalized["actor"],
        )
        return {
            "accepted": True,
            "indexed": False,
            "story_bound": True,
            "cache_recorded": cache_recorded,
            "story_id": story_id,
            "project_id": normalized["project_id"],
            "commit_sha": normalized["commit_sha"],
            "changed_files": normalized["changed_files"],
        }

    def _resolve_provider(self, *, provider: str | None, payload: dict, headers: dict[str, str]) -> str:
        value = (provider or "").strip().lower()
        if value in {"gitea", "bitbucket"}:
            return value

        lowered = {str(k).lower(): str(v) for k, v in headers.items()}
        if "x-gitea-event" in lowered:
            return "gitea"
        if "x-event-key" in lowered:
            return "bitbucket"

        if isinstance(payload.get("commits"), list) and ("ref" in payload or "pusher" in payload):
            return "gitea"
        push = payload.get("push")
        if isinstance(push, dict) and isinstance(push.get("changes"), list):
            return "bitbucket"
        return ""

    def _normalize(self, *, provider: str, payload: dict) -> dict | None:
        key = provider.lower().strip()
        if key == "gitea":
            return self._normalize_gitea(payload)
        if key == "bitbucket":
            return self._normalize_bitbucket(payload)
        return None

    def _normalize_gitea(self, payload: dict) -> dict:
        repo = payload.get("repository") or {}
        commits = payload.get("commits") or []
        project_id = str(repo.get("full_name") or repo.get("name") or "unknown_repo")
        ref = str(payload.get("ref") or "")
        branch = ref.replace("refs/heads/", "") if ref.startswith("refs/heads/") else ref or None
        actor = str((payload.get("pusher") or {}).get("username") or "") or None

        messages: list[str] = []
        changed_files: set[str] = set()
        commit_sha: str | None = None
        for commit in commits:
            if not isinstance(commit, dict):
                continue
            cid = str(commit.get("id") or "").strip()
            if cid:
                commit_sha = cid
            msg = str(commit.get("message") or "").strip()
            if msg:
                messages.append(msg)
            for key in ("added", "modified", "removed"):
                for path in commit.get(key) or []:
                    path_value = str(path).strip()
                    if path_value:
                        changed_files.add(path_value)

        summary = messages[-1] if messages else "Webhook commit without message"
        return {
            "project_id": project_id,
            "branch": branch,
            "commit_sha": commit_sha,
            "changed_files": sorted(changed_files),
            "messages": messages,
            "summary": summary,
            "actor": actor,
        }

    def _normalize_bitbucket(self, payload: dict) -> dict:
        repo = payload.get("repository") or {}
        project_id = str(repo.get("full_name") or repo.get("name") or "unknown_repo")

        changes = (((payload.get("push") or {}).get("changes")) or [])
        messages: list[str] = []
        changed_files: set[str] = set()
        commit_sha: str | None = None
        branch: str | None = None
        actor = None

        actor_raw = payload.get("actor") or {}
        if isinstance(actor_raw, dict):
            actor = str(actor_raw.get("display_name") or actor_raw.get("username") or "") or None

        for change in changes:
            if not isinstance(change, dict):
                continue
            new_ref = change.get("new") or {}
            if isinstance(new_ref, dict):
                branch_name = str(new_ref.get("name") or "").strip()
                if branch_name:
                    branch = branch_name
                target = new_ref.get("target") or {}
                if isinstance(target, dict):
                    h = str(target.get("hash") or "").strip()
                    if h:
                        commit_sha = h
                    msg = str(target.get("message") or "").strip()
                    if msg:
                        messages.append(msg)

            for commit in change.get("commits") or []:
                if not isinstance(commit, dict):
                    continue
                h = str(commit.get("hash") or "").strip()
                if h:
                    commit_sha = h
                msg = str(commit.get("message") or "").strip()
                if msg:
                    messages.append(msg)
                for key in ("added", "modified", "removed"):
                    for item in commit.get(key) or []:
                        if isinstance(item, dict):
                            path_value = str(item.get("path") or "").strip()
                        else:
                            path_value = str(item).strip()
                        if path_value:
                            changed_files.add(path_value)

        summary = messages[-1] if messages else "Webhook commit without message"
        return {
            "project_id": project_id,
            "branch": branch,
            "commit_sha": commit_sha,
            "changed_files": sorted(changed_files),
            "messages": messages,
            "summary": summary,
            "actor": actor,
        }

    def _extract_story_id(self, messages: list[str]) -> str | None:
        for msg in messages:
            match = _STORY_ID_RE.search(msg)
            if match:
                return match.group(0)
        return None
