"""Legacy Stream resource routes; public paths and operation names are preserved."""

from typing import Optional
from fastapi import APIRouter, HTTPException, Response, Request
from pydantic import BaseModel, Field, ConfigDict, model_validator, StrictInt, StrictStr
from app.application.streams import stream_service
from app.runtime_defaults import ACTIVE_LEARNING_DEFAULT, STREAM_AUTOSTART_DEFAULT, STREAM_START_DEFAULT
from app.source_adapters.rtsp import RtspSourceOptions
from app.stream_lifecycle import lifecycle_command_payload
from app.stream_config_store import ConfigPersistenceError
from app.frame_time import FrameAnchor

router = APIRouter()


class StreamSpec(BaseModel):
    stream_id: str
    server_id: Optional[str] = Field(default=None, min_length=1, max_length=256, strict=True)
    source_id: Optional[str] = Field(default=None, min_length=1, max_length=256, strict=True)
    camera_id: StrictInt | StrictStr | None = None
    source_type: str = "rtsp"          # rtsp|hls|file|onvif|webrtc
    uri: Optional[str] = None          # rtsp://, http(s)://(hls), /abs/path(file), webrtc://
    host: Optional[str] = None         # onvif
    port: Optional[int] = None         # onvif (default 80)
    username: Optional[str] = None     # onvif
    password: Optional[str] = None     # onvif
    profile_token: Optional[str] = None  # onvif (None=자동 선택)
    site_id: Optional[str] = "SITE01"
    store: bool = True
    persist_every: int = 1
    persist_mode: str = "all"          # all | event_gated — 이벤트 게이트 영속화 (이벤트 윈도우 프레임만 저장)
    scenarios: str = "all"             # all | off | name1,name2
    active_learning: bool = ACTIVE_LEARNING_DEFAULT
    autostart: bool = STREAM_AUTOSTART_DEFAULT
    optional_roles: list[str] = Field(default_factory=list)  # 명시 role만 running Gate에서 제외
    inference_profile: Optional[dict] = None  # Phase 1 — {det_model, roles, thresholds...} (카탈로그 검증)
    frame_time_anchor: Optional[FrameAnchor] = None
    intrusion: Optional[dict] = None
    loitering: Optional[dict] = None
    fall: Optional[dict] = None
    tracking: Optional[dict] = None
    analysis_fps_target: float = Field(default=10, ge=1, le=60, strict=True, alias='analysisFpsTarget')
    source_options: Optional[RtspSourceOptions] = Field(
        default=None,
        description="RTSP URI와 분리된 입력 동작 설정. 미지정 시 안전한 기본값을 사용한다.",
    )

    @model_validator(mode="after")
    def canonical_identity_pair(self):
        # RTSPTEST-02: internal Swagger registration must retain the Redis channel identity.
        if (self.server_id is None) != (self.source_id is None):
            raise ValueError("server_id and source_id must be supplied together")
        if self.server_id is not None and (not self.server_id.strip() or not self.source_id.strip()):
            raise ValueError("Canonical identity cannot be blank")
        if self.camera_id is not None and str(self.camera_id)!=self.source_id:
            raise ValueError('External cameraId and canonical source_id differ')
        if type(self.camera_id) is int and not 0 < self.camera_id <= 2**53-1:
            raise ValueError('Numeric cameraId must be a positive JSON-safe integer')
        return self


def _sm():
    from app.stream_manager import SourceError
    return stream_service, SourceError


class SettingsPatch(BaseModel):
    fight: Optional[dict] = None
    crowd: Optional[dict] = None
    fall: Optional[dict] = None
    tracking: Optional[dict] = None
    fire: Optional[dict] = None
    pose: Optional[dict] = None
    model_config = ConfigDict(extra='forbid', allow_inf_nan=False)
    expected_revision: str = Field(pattern=r'^[a-f0-9]{64}$')
    display_score: Optional[float] = Field(default=None, ge=0, le=1, strict=True)
    downstream_score: Optional[float] = Field(default=None, gt=0, le=1, strict=True)
    frame_time_anchor: Optional[FrameAnchor] = None
    analysisFpsTarget: Optional[float] = Field(default=None, ge=1, le=60, strict=True)
    intrusion: Optional[dict] = None
    loitering: Optional[dict] = None
    display_categories: Optional[list[str]] = None
    debug_all_candidates: Optional[bool] = Field(default=None, strict=True)


class MetricsWindow(BaseModel):
    model_config = ConfigDict(extra='forbid')
    run_generation: int = Field(ge=1, strict=True)
    enabled: bool = Field(strict=True)
    seconds: int = Field(default=60, ge=1, le=60, strict=True)


@router.post('/api/v1/streams/{stream_id}/metrics')
def metrics_window(stream_id: str, request: MetricsWindow):
    group = stream_service.get(stream_id)
    if group is None:
        raise HTTPException(404, 'Stream not found.')
    if hasattr(group, 'configure_metrics'):
        try:
            return group.configure_metrics(request.run_generation, request.enabled, request.seconds)
        except RuntimeError:
            raise HTTPException(409, 'Child measurement run is unavailable.') from None
    with group._teardown_lock:
        path = group.result_path
        if (group.state != 'running' or path is None
                or path.ingress.identity[4] != request.run_generation):
            raise HTTPException(409, 'Measurement run is not active.')
        try:
            path.ingress.metrics.configure(request.enabled, request.seconds)
            return path.ingress.metrics.snapshot()
        except RuntimeError:
            raise HTTPException(409, 'Measurement run is closed.') from None


@router.get('/api/v1/metadata/status')
def metadata_status(request: Request, include_timeline: bool = False):
    result = stream_service.metadata_status(include_timeline=include_timeline)
    artifacts = getattr(request.app.state,'event_artifacts',None)
    result['artifacts'] = artifacts.status() if artifacts is not None else {'enabled':False}
    return result


class PublisherMetricsWindow(BaseModel):
    model_config = ConfigDict(extra='forbid')
    enabled: bool = Field(strict=True)
    seconds: int = Field(default=30, ge=1, le=60, strict=True)


@router.post('/api/v1/metadata/metrics')
def publisher_metrics_window(request: PublisherMetricsWindow):
    publisher = stream_service.manager.metadata_publisher
    if publisher is None:
        raise HTTPException(409, 'Publisher is not active.')
    try:
        return publisher.configure_metrics(request.enabled, request.seconds)
    except RuntimeError:
        raise HTTPException(409, 'Publisher diagnostics are unavailable.') from None


@router.get('/api/v1/streams/{stream_id}/settings')
def get_settings(stream_id: str):
    try:
        return stream_service.settings(stream_id)
    except KeyError:
        raise HTTPException(404, 'Stream not found.') from None
    except ConfigPersistenceError:
        raise HTTPException(503, 'Configuration could not be read.') from None


@router.patch('/api/v1/streams/{stream_id}/settings')
def patch_settings(stream_id: str, request: SettingsPatch):
    try:
        return stream_service.patch_settings(stream_id, request.expected_revision,
            request.model_dump(exclude_unset=True, exclude={'expected_revision'}))
    except KeyError:
        raise HTTPException(404, 'Stream not found.') from None
    except ConfigPersistenceError:
        raise HTTPException(503, 'Configuration was not saved.') from None
    except (ValueError, RuntimeError):
        raise HTTPException(409, 'Configuration conflicts or cleanup is incomplete.') from None
    except OSError:
        raise HTTPException(503, 'Configuration was not saved.') from None


@router.get("/api/v1/streams")
def list_streams(detail: bool = False):
    """모든 스트림 상태 (detail=true 시 최신 프레임 탐지 요약 포함)"""
    manager, _ = _sm()
    return {"streams": manager.list(detail=detail)}


@router.post("/api/v1/streams", status_code=201)
def add_stream(spec: StreamSpec, start: bool = STREAM_START_DEFAULT):
    """스트림을 등록한다. 실제 실행은 명시적인 start 요청으로만 수행한다.

    소스 해석 실패(onvif 인증/발견 등)는 201 + state=error + last_error 로 반환 —
    스트림은 레지스트리에 등록되므로 수정 후 /restart 가능."""
    manager, _ = _sm()
    d = spec.model_dump(exclude_none=True)
    if "site_id" in spec.model_fields_set:
        d["site_id"] = spec.site_id
    try:
        g = manager.add(d, start=start)
    except ValueError as e:
        raise HTTPException(status_code=409, detail="Stream request conflicts.")
    return {"stream_id": g.stream_id, "state": g.state, "last_error": g.last_error}


@router.get("/api/v1/streams/{stream_id}")
def stream_status(stream_id: str):
    manager, _ = _sm()
    g = manager.get(stream_id)
    if g is None:
        raise HTTPException(status_code=404, detail="Stream not found.")
    return g.status(detail=True)


@router.delete("/api/v1/streams/{stream_id}")
def remove_stream(stream_id: str):
    manager, _ = _sm()
    try:
        manager.remove(stream_id)
    except KeyError as e:
        raise HTTPException(status_code=404, detail="Stream not found.")
    except RuntimeError:
        raise HTTPException(status_code=409, detail="Stream cleanup is incomplete.")
    return {"removed": stream_id}


@router.post("/api/v1/streams/{stream_id}/start", status_code=202)
def start_stream(stream_id: str):
    manager, _ = _sm()
    try:
        g = manager.start(stream_id)
    except KeyError as e:
        raise HTTPException(status_code=404, detail="Stream not found.")
    except RuntimeError:
        raise HTTPException(status_code=409, detail="Stream cleanup is incomplete.")
    return lifecycle_command_payload(stream_id, g.lifecycle.snapshot())


@router.post("/api/v1/streams/{stream_id}/stop")
def stop_stream(stream_id: str, response: Response):
    manager, _ = _sm()
    try:
        g = manager.stop(stream_id)
    except KeyError as e:
        raise HTTPException(status_code=404, detail="Stream not found.")
    state = g.lifecycle.snapshot()
    if state["observed_state"] == "stopping":
        response.status_code = 202
    return lifecycle_command_payload(stream_id, state)


@router.post("/api/v1/streams/{stream_id}/restart")
def restart_stream(stream_id: str, response: Response):
    manager, _ = _sm()
    try:
        g = manager.restart(stream_id)
    except KeyError as e:
        raise HTTPException(status_code=404, detail="Stream not found.")
    except RuntimeError:
        raise HTTPException(status_code=409, detail="Stream cleanup is incomplete or another configuration is pending.")
    state = g.lifecycle.snapshot()
    if state["observed_state"] in ("stopping", "starting"):
        response.status_code = 202
    return lifecycle_command_payload(stream_id, state)


@router.post("/api/v1/streams/{stream_id}/onvif-discover")
def onvif_discover(stream_id: str):
    """ONVIF 스트림의 디바이스 정보 + 프로파일 + 스트림 URI 미리보기 (기동 없이)."""
    manager, SourceError = _sm()
    g = manager.get(stream_id)
    if g is None:
        raise HTTPException(status_code=404, detail="Stream not found.")
    from app.onvif_discovery import resolve_onvif_stream, OnvifError
    try:
        res = resolve_onvif_stream(
            host=g.spec.get("host"), port=int(g.spec.get("port") or 80),
            username=g.spec.get("username"), password=g.spec.get("password"),
            profile_token=g.spec.get("profile_token"))
    except (OnvifError, SourceError) as e:
        raise HTTPException(status_code=502, detail="Source discovery failed.")
    return res
