"""Business logic: link GitHub user-app installations and project mirror targets."""
from __future__ import annotations

import re
import urllib.parse
from collections.abc import Awaitable, Callable
from datetime import datetime, timedelta, timezone
from typing import Optional
from uuid import UUID

import structlog
from jose import JWTError, jwt

from src.config import get_settings
from src.core.exceptions import ForbiddenError, NotFoundError, ValidationError
from src.core.security.encryption import encrypt_value
from src.domain.github_user_mirror.repository import (
    ProjectUserMirrorRepository,
    WorkspaceUserGithubInstallationRepository,
)
from src.domain.projects.repository import ProjectRepository
from src.integrations.github_user_app_client import GitHubUserAppClient

logger = structlog.get_logger()

_STATE_ALG = "HS256"
_STATE_TTL_MIN = 180  # Install OAuth round-trip can exceed a short TTL if user configures repos slowly.
_INSTALLATION_URL_RE = re.compile(r"(?:^|/)installations/(\d+)(?:[/?#]|$)")


def _encode_install_state(
    workspace_id: UUID,
    user_id: UUID,
    return_url: str,
    import_project_id: Optional[UUID] = None,
) -> str:
    settings = get_settings()
    now = datetime.now(timezone.utc)
    payload = {
        "ws": str(workspace_id),
        "sub": str(user_id),
        "ru": return_url,
        "iat": now,
        "exp": now + timedelta(minutes=_STATE_TTL_MIN),
    }
    if import_project_id:
        payload["ip"] = str(import_project_id)
    return jwt.encode(payload, settings.jwt_secret, algorithm=_STATE_ALG)


def decode_github_install_state_payload(token: str) -> dict:
    """Decode install ``state`` JWT into a normalized payload."""
    settings = get_settings()
    try:
        payload = jwt.decode(token, settings.jwt_secret, algorithms=[_STATE_ALG])
    except JWTError as e:
        raise ValidationError("Invalid or expired state token") from e
    ws = payload.get("ws")
    sub = payload.get("sub")
    if not ws or not sub:
        raise ValidationError("Invalid state payload")
    ru = payload.get("ru")
    if ru is not None and not isinstance(ru, str):
        ru = None
    ip = payload.get("ip")
    import_project_id = UUID(ip) if isinstance(ip, str) and ip.strip() else None
    return {
        "workspace_id": UUID(ws),
        "user_id": UUID(sub),
        "return_url": ru,
        "import_project_id": import_project_id,
    }


def decode_github_install_state(token: str) -> tuple[UUID, UUID, Optional[str]]:
    """Decode install ``state`` JWT: workspace, user, embedded return URL (if any)."""
    payload = decode_github_install_state_payload(token)
    return payload["workspace_id"], payload["user_id"], payload["return_url"]


def _decode_install_state(token: str) -> tuple[UUID, UUID]:
    ws, uid, _ = decode_github_install_state(token)
    return ws, uid


def parse_github_installation_id(value: str) -> int:
    """Extract a GitHub App installation id from a numeric string or settings URL."""
    raw = (value or "").strip()
    if raw.isdigit():
        installation_id = int(raw)
        if installation_id > 0:
            return installation_id
    parsed = urllib.parse.urlparse(raw)
    host = (parsed.netloc or "").lower()
    if host and host not in {"github.com", "www.github.com"}:
        raise ValidationError("GitHub installation URL must be from github.com.")
    path = parsed.path if parsed.scheme or parsed.netloc else raw
    match = _INSTALLATION_URL_RE.search(path)
    if not match:
        raise ValidationError("Could not find a GitHub installation id in this URL.")
    return int(match.group(1))


class GithubUserMirrorService:
    def __init__(
        self,
        installation_repo: WorkspaceUserGithubInstallationRepository,
        mirror_repo: ProjectUserMirrorRepository,
        project_repo: ProjectRepository,
    ):
        self._inst = installation_repo
        self._mirror = mirror_repo
        self._projects = project_repo

    def build_install_url(
        self,
        workspace_id: UUID,
        user_id: UUID,
        callback_return_url: str,
        import_project_id: Optional[UUID] = None,
    ) -> tuple[str, str]:
        settings = get_settings()
        slug = (settings.github_user_app_slug or "").strip()
        if not slug:
            raise ValidationError(
                "GITHUB_USER_APP_SLUG is not configured (GitHub App slug for install URL)."
            )
        cb = (callback_return_url or "").strip()
        if not cb:
            raise ValidationError("callback_return_url is required to build the install flow.")
        state = _encode_install_state(
            workspace_id,
            user_id,
            cb,
            import_project_id=import_project_id,
        )
        base = f"https://github.com/apps/{slug}/installations/new"
        url = f"{base}?{urllib.parse.urlencode({'state': state})}"
        return url, state

    async def confirm_installation(
        self,
        *,
        workspace_id: UUID,
        user_id: UUID,
        installation_id: int,
        state: Optional[str],
    ):
        if state:
            ws_claim, user_claim = _decode_install_state(state)
            if ws_claim != workspace_id or user_claim != user_id:
                raise ForbiddenError("State does not match workspace or user")
        gh = GitHubUserAppClient()
        try:
            raw = await gh.get_installation(installation_id)
        except Exception as e:
            logger.warning("github_user_app_installation_verify_failed", error=str(e))
            raise ValidationError(
                "Could not verify this installation with GitHub. Check GITHUB_USER_APP_* config."
            ) from e

        account = raw.get("account") or {}
        existing = await self._inst.get_by_workspace_and_github_installation_id(
            workspace_id,
            installation_id,
        )

        sel = (raw.get("repository_selection") or "all").lower()
        if sel not in ("all", "selected"):
            sel = "all"

        cipher = None
        try:
            cipher = encrypt_value(str(installation_id))
        except Exception:
            cipher = None

        if existing:
            existing.account_login = str(account.get("login") or "")
            existing.account_id = account.get("id")
            existing.account_type = str(account.get("type") or "User")
            existing.repository_selection = sel
            existing.installation_id_ciphertext = cipher
            existing.suspended_at = None
            await self._inst.flush()
            return existing

        return await self._inst.create(
            workspace_id=workspace_id,
            github_installation_id=installation_id,
            installation_id_ciphertext=cipher,
            account_login=str(account.get("login") or ""),
            account_id=account.get("id"),
            account_type=str(account.get("type") or "User"),
            repository_selection=sel,
            created_by=user_id,
        )

    async def list_installation_repos(self, workspace_id: UUID, row_id: UUID) -> list[dict]:
        row = await self._inst.get_by_id(row_id)
        if not row or row.workspace_id != workspace_id:
            raise NotFoundError("Installation not found")
        if row.suspended_at is not None:
            raise ValidationError("Installation is suspended; reconnect on GitHub.")
        gh = GitHubUserAppClient()
        repos = await gh.list_installation_repositories(row.github_installation_id)
        out = []
        for r in repos:
            out.append(
                {
                    "id": r.get("id"),
                    "full_name": r.get("full_name") or "",
                    "private": bool(r.get("private")),
                    "default_branch": r.get("default_branch"),
                }
            )
        return out

    async def upsert_project_mirror(
        self,
        *,
        project_id: UUID,
        workspace_id: UUID,
        user_id: UUID,
        installation_row_id: UUID,
        target_repo_full_name: str,
        target_default_branch: str,
        enabled: bool,
        check_permission: Callable[[UUID, UUID, str], Awaitable[bool]],
    ):
        if not await check_permission(workspace_id, user_id, "repos.manage"):
            raise ForbiddenError()
        proj = await self._projects.get_by_id(project_id)
        if not proj or proj.workspace_id != workspace_id:
            raise NotFoundError("Project not found")
        inst = await self._inst.get_by_id(installation_row_id)
        if not inst or inst.workspace_id != workspace_id or inst.suspended_at is not None:
            raise ValidationError("Invalid or suspended GitHub installation for this workspace.")
        gh = GitHubUserAppClient()
        repos = await gh.list_installation_repositories(inst.github_installation_id)
        names = {r.get("full_name") for r in repos if r.get("full_name")}
        if target_repo_full_name not in names:
            raise ValidationError(
                "Target repo is not visible to this installation "
                "(check repository access on GitHub)."
            )
        return await self._mirror.upsert(
            project_id=project_id,
            workspace_user_github_installation_id=installation_row_id,
            target_repo_full_name=target_repo_full_name.strip(),
            target_default_branch=target_default_branch.strip() or "main",
            enabled=enabled,
        )
