"""
Jenkins CI/CD 연동 클라이언트.

GUARDiA ITSM → Jenkins REST API 호출 및 콜백 처리.
보안: 서버 IP, 자격증명은 환경변수에서만 로드 — 코드에 하드코딩 금지.
"""
from __future__ import annotations

import logging
import os
from typing import Any, Optional

import httpx

logger = logging.getLogger(__name__)

# ── 환경변수 ──────────────────────────────────────────────────────────────────
_JENKINS_URL   = os.getenv("JENKINS_URL",   "http://jenkins.agency.go.kr:8080")
_JENKINS_USER  = os.getenv("JENKINS_USER",  "itsm-bot")
_JENKINS_TOKEN = os.getenv("JENKINS_TOKEN", "")

# Jenkins → ITSM 콜백을 인증할 공유 시크릿
_CALLBACK_SECRET = os.getenv("JENKINS_CALLBACK_SECRET", "")

# HTTP 타임아웃 (초)
_TIMEOUT = float(os.getenv("JENKINS_TIMEOUT", "30"))


# ── 유틸 ─────────────────────────────────────────────────────────────────────
def _auth() -> tuple[str, str]:
    if not _JENKINS_TOKEN:
        raise RuntimeError(
            "JENKINS_TOKEN 환경변수가 설정되지 않았습니다. "
            "Jenkins API 토큰을 .env에 등록하세요."
        )
    return (_JENKINS_USER, _JENKINS_TOKEN)


def _sanitize_error(exc: Exception) -> str:
    """에러 메시지에서 IP·계정·비밀번호 패턴 제거."""
    import re
    msg = str(exc)
    msg = re.sub(r"\b\d{1,3}(?:\.\d{1,3}){3}\b", "<IP>", msg)
    msg = re.sub(r"(?i)(password|token|secret|key)\s*[=:]\s*\S+", r"\1=<hidden>", msg)
    return msg


# ── 파이프라인 트리거 ────────────────────────────────────────────────────────
async def trigger_pipeline(
    job_name: str,
    session_id: int,
    sr_id: str,
    deploy_env: str = "dev",
    target_server: str = "",
    skip_test: bool = False,
    extra_params: Optional[dict[str, Any]] = None,
) -> dict[str, Any]:
    """
    Jenkins 파이프라인 빌드 트리거.

    Returns:
        {"queued": True, "queue_id": "<숫자>", "queue_url": "<URL>"}
    Raises:
        RuntimeError: 트리거 실패 시
    """
    url = f"{_JENKINS_URL}/job/{job_name}/buildWithParameters"
    params: dict[str, str] = {
        "ITSM_SESSION_ID": str(session_id),
        "ITSM_SR_ID":      sr_id,
        "DEPLOY_ENV":      deploy_env,
        "TARGET_SERVER":   target_server,
        "SKIP_TEST":       str(skip_test).lower(),
    }
    if extra_params:
        params.update({k: str(v) for k, v in extra_params.items()})

    logger.info(
        "Jenkins 파이프라인 트리거: job=%s session_id=%s env=%s",
        job_name, session_id, deploy_env,
    )

    try:
        async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
            resp = await client.post(url, auth=_auth(), params=params)
    except httpx.ConnectError as exc:
        raise RuntimeError(f"Jenkins 서버 연결 실패: {_sanitize_error(exc)}") from exc
    except httpx.TimeoutException as exc:
        raise RuntimeError(f"Jenkins 요청 타임아웃 ({_TIMEOUT}s)") from exc
    except Exception as exc:
        raise RuntimeError(f"Jenkins 트리거 오류: {_sanitize_error(exc)}") from exc

    if resp.status_code not in (200, 201):
        raise RuntimeError(
            f"Jenkins 트리거 실패: HTTP {resp.status_code} "
            f"(job={job_name})"
        )

    # 큐 ID는 Location 헤더에서 추출 (예: /queue/item/42/)
    location = resp.headers.get("Location", "")
    queue_id: Optional[str] = None
    if location:
        parts = location.rstrip("/").split("/")
        queue_id = parts[-1] if parts[-1].isdigit() else None

    logger.info("Jenkins 파이프라인 큐 등록: queue_id=%s", queue_id)
    return {
        "queued":     True,
        "queue_id":   queue_id,
        "queue_url":  location,
        "job_name":   job_name,
        "session_id": session_id,
    }


# ── 빌드 상태 조회 ───────────────────────────────────────────────────────────
async def get_build_status(job_name: str, build_number: int) -> dict[str, Any]:
    """
    특정 빌드의 상태 조회.

    Returns:
        {"result": "SUCCESS|FAILURE|ABORTED|null", "building": bool, ...}
    """
    url = f"{_JENKINS_URL}/job/{job_name}/{build_number}/api/json"
    try:
        async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
            resp = await client.get(url, auth=_auth())
        resp.raise_for_status()
        data = resp.json()
        return {
            "result":        data.get("result"),
            "building":      data.get("building", False),
            "duration_ms":   data.get("duration", 0),
            "timestamp":     data.get("timestamp"),
            "url":           data.get("url", ""),
            "display_name":  data.get("displayName", ""),
        }
    except httpx.HTTPStatusError as exc:
        if exc.response.status_code == 404:
            raise RuntimeError(f"빌드를 찾을 수 없습니다: {job_name} #{build_number}") from exc
        raise RuntimeError(f"빌드 상태 조회 실패: {_sanitize_error(exc)}") from exc
    except Exception as exc:
        raise RuntimeError(f"빌드 상태 조회 오류: {_sanitize_error(exc)}") from exc


# ── 큐 아이템 → 빌드 번호 조회 ──────────────────────────────────────────────
async def get_queue_item(queue_id: str) -> dict[str, Any]:
    """
    큐 아이템에서 실제 빌드 번호를 조회.
    빌드가 시작되면 executable.number 필드가 채워진다.
    """
    url = f"{_JENKINS_URL}/queue/item/{queue_id}/api/json"
    try:
        async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
            resp = await client.get(url, auth=_auth())
        resp.raise_for_status()
        data = resp.json()
        executable = data.get("executable") or {}
        return {
            "queue_id":     queue_id,
            "build_number": executable.get("number"),
            "build_url":    executable.get("url", ""),
            "blocked":      data.get("blocked", False),
            "why":          data.get("why", ""),
        }
    except Exception as exc:
        raise RuntimeError(f"큐 아이템 조회 오류: {_sanitize_error(exc)}") from exc


# ── 빌드 취소 ────────────────────────────────────────────────────────────────
async def abort_build(job_name: str, build_number: int) -> bool:
    """실행 중인 빌드를 강제 중단."""
    url = f"{_JENKINS_URL}/job/{job_name}/{build_number}/stop"
    try:
        async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
            resp = await client.post(url, auth=_auth())
        return resp.status_code in (200, 302)
    except Exception as exc:
        logger.warning("빌드 중단 실패: %s", _sanitize_error(exc))
        return False


# ── Jenkins 연결 테스트 ───────────────────────────────────────────────────────
async def ping() -> dict[str, Any]:
    """Jenkins 서버 연결 상태 확인."""
    url = f"{_JENKINS_URL}/api/json"
    try:
        async with httpx.AsyncClient(timeout=10.0) as client:
            resp = await client.get(url, auth=_auth())
        resp.raise_for_status()
        data = resp.json()
        return {
            "ok":           True,
            "version":      resp.headers.get("X-Jenkins", "unknown"),
            "mode":         data.get("mode", ""),
            "num_executors": data.get("numExecutors", 0),
        }
    except Exception as exc:
        return {"ok": False, "error": _sanitize_error(exc)}


# ── 콜백 인증 검증 ────────────────────────────────────────────────────────────
def verify_callback_secret(received_secret: str) -> bool:
    """
    Jenkins에서 보낸 콜백 시크릿을 검증.
    JENKINS_CALLBACK_SECRET이 설정되지 않으면 검증 생략 (개발용).
    """
    if not _CALLBACK_SECRET:
        logger.warning("JENKINS_CALLBACK_SECRET 미설정 — 콜백 인증 건너뜀 (개발 전용)")
        return True
    import hmac
    return hmac.compare_digest(_CALLBACK_SECRET, received_secret)


# ── SonarQube 연동 ────────────────────────────────────────────────────────────

_SONAR_URL   = os.getenv("SONARQUBE_URL",   "http://sonarqube.agency.go.kr:9000")
_SONAR_TOKEN = os.getenv("SONARQUBE_TOKEN", "")


class SonarStatus(str):
    OK      = "OK"
    WARN    = "WARN"
    ERROR   = "ERROR"
    NONE    = "NONE"


class SonarResult:
    """SonarQube Quality Gate 결과 데이터 클래스."""

    __slots__ = (
        "project_key", "status", "conditions",
        "raw_url", "error_msg",
    )

    def __init__(
        self,
        project_key: str,
        status: str,
        conditions: list[dict[str, Any]],
        raw_url: str = "",
        error_msg: str = "",
    ) -> None:
        self.project_key = project_key
        self.status      = status          # OK | WARN | ERROR | NONE
        self.conditions  = conditions      # [{"metric": ..., "status": ..., "actual": ...}]
        self.raw_url     = raw_url
        self.error_msg   = error_msg

    @property
    def is_ok(self) -> bool:
        return self.status == "OK"

    @property
    def failed_conditions(self) -> list[dict[str, Any]]:
        return [c for c in self.conditions if c.get("status") == "ERROR"]

    def to_dict(self) -> dict[str, Any]:
        return {
            "project_key":        self.project_key,
            "status":             self.status,
            "conditions":         self.conditions,
            "failed_conditions":  self.failed_conditions,
            "raw_url":            self.raw_url,
            "error_msg":          self.error_msg,
        }


async def _get_vibe_session(session_id: int) -> Any:
    """VibeSession 레코드 조회 헬퍼."""
    from sqlalchemy import select
    from database import SessionLocal
    from models import VibeSession
    async with SessionLocal() as db:
        return (await db.execute(
            select(VibeSession).where(VibeSession.id == session_id)
        )).scalars().first()


async def _resolve_job_name(session_id: int) -> Optional[str]:
    """
    VibeSession.project_id -> Project.jenkins_job_name 으로
    Jenkins 잡 이름을 조회합니다.
    project_id 없거나 jenkins_job_name 미설정 시 None 반환.
    """
    from sqlalchemy import select
    from database import SessionLocal
    from models import VibeSession, Project
    async with SessionLocal() as db:
        vs = (await db.execute(
            select(VibeSession).where(VibeSession.id == session_id)
        )).scalars().first()
        if not vs or not vs.project_id:
            return None
        proj = (await db.execute(
            select(Project).where(Project.id == vs.project_id)
        )).scalars().first()
        if not proj:
            return None
        return getattr(proj, "jenkins_job_name", None)


async def get_sonarqube_result(project_key: str) -> SonarResult:
    """
    SonarQube Quality Gate 상태 조회.

    API: GET {SONARQUBE_URL}/api/qualitygates/project_status?projectKey={project_key}

    Returns:
        SonarResult(status="OK"|"WARN"|"ERROR"|"NONE", conditions=[...])
    Raises:
        RuntimeError: API 호출 실패 시
    """
    if not _SONAR_TOKEN:
        logger.warning("SONARQUBE_TOKEN 미설정 — SonarQube 연동 불가")
        return SonarResult(project_key=project_key, status="NONE", conditions=[],
                           error_msg="SONARQUBE_TOKEN 미설정")

    url = f"{_SONAR_URL}/api/qualitygates/project_status"
    headers = {"Authorization": f"Bearer {_SONAR_TOKEN}"}

    try:
        async with httpx.AsyncClient(timeout=30.0) as client:
            resp = await client.get(url, headers=headers,
                                    params={"projectKey": project_key})
        resp.raise_for_status()
        data = resp.json()
    except httpx.HTTPStatusError as exc:
        if exc.response.status_code == 404:
            return SonarResult(project_key=project_key, status="NONE", conditions=[],
                               error_msg=f"프로젝트를 찾을 수 없습니다: {project_key}")
        raise RuntimeError(f"SonarQube API 오류: HTTP {exc.response.status_code}") from exc
    except Exception as exc:
        raise RuntimeError(f"SonarQube 조회 실패: {_sanitize_error(exc)}") from exc

    pj_status = data.get("projectStatus", {})
    gate_status = pj_status.get("status", "NONE")   # OK | WARN | ERROR | NONE

    # conditions 정규화
    raw_conditions = pj_status.get("conditions", [])
    conditions: list[dict[str, Any]] = []
    for c in raw_conditions:
        conditions.append({
            "metric":         c.get("metricKey", ""),
            "status":         c.get("status", ""),
            "actual":         c.get("actualValue", ""),
            "error_threshold": c.get("errorThreshold", ""),
            "comparator":     c.get("comparator", ""),
        })

    raw_url = f"{_SONAR_URL}/dashboard?id={project_key}"
    logger.info("SonarQube QG: project=%s status=%s", project_key, gate_status)
    return SonarResult(
        project_key = project_key,
        status      = gate_status,
        conditions  = conditions,
        raw_url     = raw_url,
    )


async def handle_sonar_gate_failure(
    session_id: int,
    result: SonarResult,
    db: Any,   # AsyncSession — Any to avoid circular import
) -> Optional[str]:
    """
    SonarQube Quality Gate ERROR 시 SR 자동 생성.

    조건:
      - result.status == "ERROR"
      - 동일 session_id + 오늘 날짜 SR이 없을 때만 생성 (중복 방지)

    inst_id 조회 경로:
        VibeSession.sr_id -> SRRequest.inst_id

    Returns:
        생성된 SR ID 또는 None (스킵된 경우)
    """
    if result.status != "ERROR":
        return None

    from datetime import date
    from uuid import uuid4
    from sqlalchemy import select
    from models import SRRequest, SRStatus, SRType, Priority, VibeSession

    # VibeSession 조회
    vs_row = (await db.execute(
        select(VibeSession).where(VibeSession.id == session_id)
    )).scalars().first()

    if not vs_row:
        logger.warning("SonarQube: VibeSession session_id=%s 없음 — SR 미생성", session_id)
        return None

    # inst_id: VibeSession.sr_id -> SRRequest.inst_id
    inst_id: Optional[int] = None
    if vs_row.sr_id:
        sr_parent = (await db.execute(
            select(SRRequest).where(SRRequest.sr_id == vs_row.sr_id)
        )).scalars().first()
        if sr_parent:
            inst_id = sr_parent.inst_id

    # 중복 SR 방지: 오늘 동일 session_id에 대한 SONAR SR 확인
    today_prefix = f"SONAR-{date.today().strftime('%Y%m%d')}-{session_id}-"
    dup = (await db.execute(
        select(SRRequest).where(SRRequest.sr_id.startswith(today_prefix))
    )).scalars().first()
    if dup:
        logger.info("SonarQube SR 중복 스킵: %s", dup.sr_id)
        return dup.sr_id

    sr_id = f"SONAR-{date.today().strftime('%Y%m%d')}-{session_id}-{str(uuid4())[:4].upper()}"

    # 실패한 조건 요약
    failed = result.failed_conditions
    condition_summary = "\n".join(
        f"  - {c['metric']}: {c['actual']} (기준: {c['comparator']} {c['error_threshold']})"
        for c in failed
    ) if failed else "  (상세 없음)"

    description = (
        f"SonarQube Quality Gate 실패\n"
        f"프로젝트: {result.project_key}\n"
        f"상태: {result.status}\n"
        f"실패 조건:\n{condition_summary}\n\n"
        f"SonarQube 대시보드: {result.raw_url}\n"
        f"배포 세션: {session_id}"
    )

    sr = SRRequest(
        sr_id         = sr_id,
        sr_type       = SRType.OTHER,
        priority      = Priority.HIGH,
        status        = SRStatus.RECEIVED,
        title         = f"[SonarQube] Quality Gate 실패 - {result.project_key}",
        description   = description,
        inst_id       = inst_id,
        requested_by  = "sonarqube-bot",
    )
    db.add(sr)
    await db.commit()
    await db.refresh(sr)

    logger.warning(
        "SonarQube Quality Gate 실패 SR 생성: sr_id=%s project=%s",
        sr_id, result.project_key,
    )
    return sr_id


# ── Jenkins 빌드 로그 SSE 스트리밍 ─────────────────────────────────────────────

async def get_progressive_log(
    session_id: int,
    offset: int = 0,
) -> tuple[str, int]:
    """
    Jenkins progressive log 폴링.

    Jenkins API: GET {job_url}/logText/progressiveText?start={offset}
    응답 헤더의 X-Text-Size 가 다음 offset.

    job_name 조회 경로:
        VibeSession.project_id -> Project.jenkins_job_name
    build_number: VibeSession 에 별도 컬럼 없음 -> "lastBuild" 사용

    Returns:
        (log_chunk: str, next_offset: int)
    """
    vs = await _get_vibe_session(session_id)
    if not vs:
        return ("VibeSession을 찾을 수 없습니다.", offset)

    job_name = await _resolve_job_name(session_id)
    if not job_name:
        return ("[빌드 정보 없음 - project.jenkins_job_name 미설정]", offset)

    # build_number: VibeSession 에 별도 저장하지 않으므로 lastBuild 사용
    # (트리거 후 최신 빌드 = 현재 진행 중인 빌드)
    build_ref = "lastBuild"
    url = f"{_JENKINS_URL}/job/{job_name}/{build_ref}/logText/progressiveText"
    params = {"start": str(offset)}

    try:
        async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
            resp = await client.get(url, auth=_auth(), params=params)
        if resp.status_code == 404:
            return ("[로그를 찾을 수 없습니다]", offset)
        resp.raise_for_status()

        chunk       = resp.text
        next_offset = int(resp.headers.get("X-Text-Size", offset + len(chunk.encode())))
        return (chunk, next_offset)
    except Exception as exc:
        logger.warning("Jenkins 로그 조회 실패: %s", _sanitize_error(exc))
        return (f"[로그 조회 오류: {_sanitize_error(exc)}]", offset)


async def is_build_complete(session_id: int) -> bool:
    """
    Jenkins 빌드가 완료(또는 오류)되었는지 확인.

    VibeSession.project_id -> Project.jenkins_job_name 으로 job 조회.
    lastBuild 기준으로 building 상태를 확인.

    Returns:
        True  — building=False (완료/실패/중단) 또는 VibeSession 세션이 최종 상태
        False — building=True 또는 정보 없음
    """
    from models import VibeSession, VibeSessionStatus

    vs = await _get_vibe_session(session_id)
    if not vs:
        return True   # 세션 없음 → 스트리밍 종료

    # VibeSession 자체가 최종 상태면 빌드 완료로 간주
    final_statuses = {
        VibeSessionStatus.COMPLETED,
        VibeSessionStatus.FAILED,
        VibeSessionStatus.CANCELLED,
    }
    if vs.status in final_statuses:
        return True

    job_name = await _resolve_job_name(session_id)
    if not job_name:
        return False   # 아직 job 정보 없음

    try:
        status = await get_build_status(job_name, "lastBuild")
        return not status.get("building", True)
    except Exception as exc:
        logger.warning("빌드 상태 확인 실패: %s", _sanitize_error(exc))
        return False


# ── 빌드 이력 조회 ────────────────────────────────────────────────────────────

class BuildInfo:
    """Jenkins 빌드 이력 항목."""

    __slots__ = ("number", "result", "building", "duration_ms", "timestamp", "url")

    def __init__(self, **kwargs: Any) -> None:
        for k in self.__slots__:
            setattr(self, k, kwargs.get(k))

    def to_dict(self) -> dict[str, Any]:
        return {k: getattr(self, k) for k in self.__slots__}


async def get_build_history(
    project_name: str,
    limit: int = 10,
) -> list[BuildInfo]:
    """
    Jenkins 프로젝트의 최근 빌드 이력 조회.

    Returns:
        list[BuildInfo] (최신순)
    """
    url = f"{_JENKINS_URL}/job/{project_name}/api/json"
    params = {
        "tree": (
            "builds[number,result,building,duration,timestamp,url]"
            f"{{0,{limit}}}"
        )
    }
    try:
        async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
            resp = await client.get(url, auth=_auth(), params=params)
        resp.raise_for_status()
        data = resp.json()
    except Exception as exc:
        raise RuntimeError(f"빌드 이력 조회 실패: {_sanitize_error(exc)}") from exc

    builds: list[BuildInfo] = []
    for b in data.get("builds", []):
        builds.append(BuildInfo(
            number      = b.get("number"),
            result      = b.get("result"),
            building    = b.get("building", False),
            duration_ms = b.get("duration", 0),
            timestamp   = b.get("timestamp"),
            url         = b.get("url", ""),
        ))
    return builds


async def get_artifact_url(
    project_name: str,
    build_number: int,
    artifact: str,
) -> str:
    """
    Jenkins 특정 빌드의 아티팩트 다운로드 URL 생성.

    artifact 예: "target/app.jar", "dist/app.zip"

    Returns:
        직접 다운로드 URL (인증 없이 접근 가능하도록 Jenkins 설정 필요)
    """
    # artifact 경로 traversal 방지
    import re
    safe_artifact = re.sub(r"\.\./", "", artifact).lstrip("/")
    return (
        f"{_JENKINS_URL}/job/{project_name}/{build_number}"
        f"/artifact/{safe_artifact}"
    )
