import json
import logging
from pathlib import Path
from typing import Optional, Dict, Any

from fastapi import APIRouter, Depends, Request, UploadFile, File
from pydantic import BaseModel
from sqlalchemy.orm import Session as OrmSession

from app.api.deps import get_db, get_current_user, require_admin, require_project_write, verify_project_access
from app.models.catalog import UserAccount, ProjectMember
from app.schemas.common import PaginatedResponse
from app.schemas.project import (
    ProjectCreate,
    ProjectUpdate,
    ProjectResponse,
    MemberAdd,
    MemberUpdate,
    MemberResponse,
)
from app.services.project_service import ProjectService

logger = logging.getLogger(__name__)


class ActivityResponse(BaseModel):
    id: str
    actor_id: str
    actor_username: str = ""
    action: str
    resource_type: str
    resource_id: Optional[str] = None
    project_id: Optional[str] = None
    detail_json: str
    ip_address: Optional[str] = None
    created_at: str

    model_config = {"from_attributes": True}


def _enrich_activity(a, db: OrmSession) -> dict:
    d = ActivityResponse.model_validate(a).model_dump()
    user = db.query(UserAccount).filter(UserAccount.id == a.actor_id).first()
    d["actor_username"] = user.display_name if user else a.actor_id
    return d


router = APIRouter(prefix="/api/v1/projects", tags=["projects"])
activity_router = APIRouter(prefix="/api/v1/activities", tags=["activities"])


# ── projects ──────────────────────────────────────────────────

@router.get("/", response_model=PaginatedResponse[ProjectResponse])
def list_projects(
    page: int = 1,
    per_page: int = 20,
    current_user: UserAccount = Depends(get_current_user),
    db: OrmSession = Depends(get_db),
):
    service = ProjectService(db)
    items, total = service.list_projects(current_user, page, per_page)
    def to_response(p):
        d = ProjectResponse.model_validate(p).model_dump()
        creator = db.query(UserAccount).filter(UserAccount.id == p.created_by).first()
        d["created_by"] = creator.display_name if creator else p.created_by
        member = db.query(ProjectMember).filter(
            ProjectMember.project_id == p.id, ProjectMember.user_id == current_user.id
        ).first()
        d["my_role"] = member.role if member else ("admin" if current_user.role == "admin" else None)
        d["member_count"] = db.query(ProjectMember).filter(ProjectMember.project_id == p.id).count()
        return d

    return PaginatedResponse(
        items=[to_response(p) for p in items],
        total=total,
        page=page,
        per_page=per_page,
    )


@router.post("/", response_model=ProjectResponse)
def create_project(
    body: ProjectCreate,
    request: Request,
    current_user: UserAccount = Depends(get_current_user),
    db: OrmSession = Depends(get_db),
):
    service = ProjectService(db)
    ip = request.client.host if request.client else None
    project = service.create_project(body.name, body.description or "", current_user, ip)
    return project


# ── Runtime Feature Flags (must be before /{project_id} to avoid path capture) ──

FEATURE_FLAGS = {"fal_ai_enabled"}


@router.get("/feature-flags")
def get_feature_flags(
    current_user: UserAccount = Depends(get_current_user),
):
    """현재 런타임 feature flag 조회."""
    from app.core.config import settings
    return {k: getattr(settings, k, False) for k in FEATURE_FLAGS}


class FeatureFlagUpdate(BaseModel):
    flag: str
    value: bool


@router.put("/feature-flags")
def update_feature_flag(
    body: FeatureFlagUpdate,
    current_user: UserAccount = Depends(require_admin),
):
    """런타임 feature flag 토글 (서버 재시작 불필요)."""
    from app.core.config import settings

    if body.flag not in FEATURE_FLAGS:
        from app.core.errors import AppError
        raise AppError(
            code="feature_flag.invalid",
            message=f"유효하지 않은 flag: {body.flag}. 가능: {', '.join(sorted(FEATURE_FLAGS))}",
            status_code=400,
        )

    setattr(settings, body.flag, body.value)
    logging.getLogger(__name__).info("Feature flag %s → %s (by %s)", body.flag, body.value, current_user.username)
    return {"ok": True, "flag": body.flag, "value": body.value}


@router.get("/{project_id}")
def get_project(
    project_id: str,
    current_user: UserAccount = Depends(get_current_user),
    db: OrmSession = Depends(get_db),
):
    service = ProjectService(db)
    return service.get_project(project_id, current_user)


@router.patch("/{project_id}", response_model=ProjectResponse)
def update_project(
    project_id: str,
    body: ProjectUpdate,
    request: Request,
    current_user: UserAccount = Depends(get_current_user),
    db: OrmSession = Depends(get_db),
):
    service = ProjectService(db)
    ip = request.client.host if request.client else None
    updates = body.model_dump(exclude_none=True)
    return service.update_project(project_id, updates, current_user, ip)


@router.delete("/{project_id}", response_model=ProjectResponse)
def delete_project(
    project_id: str,
    request: Request,
    current_user: UserAccount = Depends(get_current_user),
    db: OrmSession = Depends(get_db),
):
    service = ProjectService(db)
    ip = request.client.host if request.client else None
    return service.delete_project(project_id, current_user, ip)


def _invalidate_planning_checkpoints(project_id: str, db: OrmSession):
    """모든 에피소드의 planning_doc_analysis 체크포인트 + step_run 무효화 +
    project-level checkpoint 도 함께 정리."""
    from pathlib import Path
    from app.core.config import settings
    from sqlalchemy import text as sql_text
    from app.services.planning_doc_analysis_service import (
        clear_project_checkpoint,
    )

    base = Path(settings.projects_dir) / project_id / "checkpoints" / "episodes"
    if base.exists():
        for ep_dir in base.iterdir():
            manifest = ep_dir / "planning_doc_analysis" / "manifest.json"
            if manifest.exists():
                manifest.unlink()

    db.execute(sql_text(
        "UPDATE step_run SET status = 'stale' "
        "WHERE project_id = :pid AND step_id = 'planning_doc_analysis'"
    ), {"pid": project_id})
    db.commit()

    clear_project_checkpoint(project_id)


def _reject_if_episodes_exist(project_id: str, db: OrmSession, *, action: str) -> None:
    """기획서 변경(업로드/삭제) 가드 — 에피소드가 1개라도 있으면 409.

    기획서는 project-level 자원이고 episode 분석 컨텍스트의 SOT 이므로
    에피소드 업로드 후에는 변경 불가. action 은 메시지에 주입.
    """
    from app.models.project import Episode
    from app.core.errors import AppError

    ep_count = db.query(Episode).filter(Episode.project_id == project_id).count()
    if ep_count > 0:
        raise AppError(
            code="planning_doc.episodes_exist",
            message=(
                f"이미 업로드된 에피소드가 {ep_count}개 있어 기획서를 {action}"
                "할 수 없습니다. 모든 에피소드를 먼저 삭제하세요."
            ),
            status_code=409,
        )


# ── planning doc (기획서) ─────────────────────────────────────

@router.post("/{project_id}/planning-doc")
async def upload_planning_doc(
    project_id: str = Depends(verify_project_access),
    file: UploadFile = File(...),
    current_user: UserAccount = Depends(get_current_user),
    db: OrmSession = Depends(get_db),
):
    """기획서 PDF 업로드 → 파일 저장 + 텍스트 추출 → DB 저장."""
    from app.models.catalog import ProjectRegistry
    from app.modules.pdf_parser import extract_text_from_pdf
    from pathlib import Path
    from app.core.config import settings
    import shutil

    project = db.query(ProjectRegistry).filter(ProjectRegistry.id == project_id).first()
    if not project:
        from app.core.errors import AppError
        raise AppError(code="project.not_found", message="프로젝트를 찾을 수 없습니다", status_code=404)

    # 에피소드 업로드 후에는 기획서 변경 불가 (project-level SOT 보호).
    _reject_if_episodes_exist(project_id, db, action="업로드")

    # 파일 타입 + 크기 검증
    suffix = Path(file.filename or "doc.pdf").suffix.lower() or ".pdf"
    if suffix != ".pdf":
        from app.core.errors import AppError
        raise AppError(code="planning_doc.invalid_type", message="PDF 파일만 지원합니다", status_code=400)

    content = await file.read()
    if len(content) > 200 * 1024 * 1024:  # 200MB
        from app.core.errors import AppError
        raise AppError(code="planning_doc.too_large", message="파일 크기가 200MB를 초과합니다", status_code=400)

    # PDF 파일을 프로젝트 assets에 저장 (LLM이 직접 읽을 수 있도록)
    assets_dir = Path(settings.projects_dir) / project_id / "assets"
    assets_dir.mkdir(parents=True, exist_ok=True)
    pdf_path = assets_dir / "planning_doc.pdf"
    pdf_path.write_bytes(content)

    # 텍스트 추출 (DB 저장 + 다른 step 주입용 fallback)
    try:
        fulltext, page_count = extract_text_from_pdf(str(pdf_path))
    except Exception:
        fulltext, page_count = "", 0

    _log = logging.getLogger(__name__)
    if not fulltext or len(fulltext.strip()) < 100:
        _log.warning("Planning doc text extraction yielded < 100 chars — text fallback unavailable, PDF multimodal only")

    from datetime import datetime, timezone
    project.planning_doc_text = fulltext
    project.updated_at = datetime.now(timezone.utc).isoformat()
    db.commit()

    # 기존 planning_doc_analysis 체크포인트 무효화 (새 기획서로 재분석 필요)
    _invalidate_planning_checkpoints(project_id, db)

    logger = logging.getLogger(__name__)
    logger.info("Planning doc uploaded for project %s: %d chars, %d pages", project_id[:8], len(fulltext), page_count)

    # 업로드 직후 자동 분석 (백그라운드).
    from app.services.planning_doc_analysis_service import (
        dispatch_planning_doc_analysis_async,
    )
    dispatch_planning_doc_analysis_async(project_id)

    return {
        "ok": True,
        "char_count": len(fulltext),
        "page_count": page_count,
        "analysis_state": "queued",
        "message": (
            f"기획서 업로드 완료 ({page_count}페이지, {len(fulltext):,}자) — "
            "백그라운드 분석을 시작했습니다."
        ),
    }


@router.get("/{project_id}/creator-corrections")
def get_creator_corrections(
    project_id: str = Depends(verify_project_access),
    current_user: UserAccount = Depends(get_current_user),
):
    """제작자 정정 목록 조회 (wave3 — 세계 사실 override 채널).

    malformed 파일은 저장/주입 경로와 동일하게 AppError(400) fail-fast.
    """
    from app.core.creator_corrections import load_creator_corrections
    entries = load_creator_corrections(project_id, active_only=False)
    return {"corrections": entries,
            "active_count": sum(1 for e in entries if e["active"])}


@router.put("/{project_id}/creator-corrections")
def put_creator_corrections(
    body: dict,
    project_id: str = Depends(verify_project_access),
    current_user: UserAccount = Depends(get_current_user),
):
    """제작자 정정 목록 저장 (전체 치환, 구조 검증 fail-fast).

    body: {"corrections": [{"id": str, "text": str, "active": bool}]}
    저장 즉시 다음 분석 스텝 실행부터 프롬프트에 주입된다 (파일 로드 방식,
    서버 재시작 불필요). 정정 0건 = 주입 0 = 기존 프롬프트와 동일.

    주의: 정정 내용은 step config_hash 에 포함되지 않으므로 **이미 completed
    인 checkpoint 는 자동 무효화되지 않는다** — 변경된 정정을 기존 산출에
    반영하려면 해당 step 을 force 재실행하거나 scene-detail/redo-shot 을
    사용할 것.
    """
    from app.core.creator_corrections import save_creator_corrections
    entries = save_creator_corrections(project_id, body)
    logging.getLogger(__name__).info(
        "creator_corrections updated: project=%s entries=%d (by %s)",
        project_id, len(entries), current_user.username)
    return {"ok": True, "corrections": entries,
            "note": "existing completed checkpoints require force/redo to"
                    " consume changed corrections"}


@router.get("/{project_id}/planning-doc")
def get_planning_doc(
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
):
    """기획서 텍스트 조회."""
    from app.models.catalog import ProjectRegistry
    project = db.query(ProjectRegistry).filter(ProjectRegistry.id == project_id).first()
    if not project:
        from app.core.errors import AppError
        raise AppError(code="project.not_found", message="프로젝트를 찾을 수 없습니다", status_code=404)

    from app.core.config import settings
    from app.services.planning_doc_analysis_service import load_status
    from app.models.project import Episode

    pdf_path = Path(settings.projects_dir) / project_id / "assets" / "planning_doc.pdf"
    try:
        pdf_stat = pdf_path.stat()
        has_pdf = True
        pdf_size = pdf_stat.st_size
    except (FileNotFoundError, OSError):
        has_pdf = False
        pdf_size = 0

    ep_count = db.query(Episode).filter(Episode.project_id == project_id).count()
    status = load_status(project_id)
    return {
        "has_planning_doc": bool(project.planning_doc_text) or has_pdf,
        "has_pdf_file": has_pdf,
        "pdf_size_bytes": pdf_size,
        "char_count": len(project.planning_doc_text) if project.planning_doc_text else 0,
        "text": project.planning_doc_text,
        "episode_count": ep_count,
        "locked": ep_count > 0,
        "analysis": status,
    }


@router.delete("/{project_id}/planning-doc")
def delete_planning_doc(
    project_id: str = Depends(verify_project_access),
    current_user: UserAccount = Depends(get_current_user),
    db: OrmSession = Depends(get_db),
):
    """기획서 삭제."""
    from app.models.catalog import ProjectRegistry
    from datetime import datetime, timezone
    project = db.query(ProjectRegistry).filter(ProjectRegistry.id == project_id).first()
    if not project:
        from app.core.errors import AppError
        raise AppError(code="project.not_found", message="프로젝트를 찾을 수 없습니다", status_code=404)

    # 에피소드 업로드 후에는 기획서 삭제 불가 (project-level SOT 보호).
    _reject_if_episodes_exist(project_id, db, action="삭제")

    project.planning_doc_text = None
    project.updated_at = datetime.now(timezone.utc).isoformat()
    db.commit()

    # PDF 파일 삭제
    from app.core.config import settings
    pdf_path = Path(settings.projects_dir) / project_id / "assets" / "planning_doc.pdf"
    if pdf_path.exists():
        pdf_path.unlink()

    # 기존 체크포인트 + step_run 무효화
    _invalidate_planning_checkpoints(project_id, db)

    return {"ok": True, "message": "기획서 삭제 완료"}


# ── members ───────────────────────────────────────────────────

def _enrich_member(m, db: OrmSession) -> dict:
    user = db.query(UserAccount).filter(UserAccount.id == m.user_id).first()
    d = MemberResponse.model_validate(m).model_dump()
    d["username"] = user.username if user else m.user_id
    d["display_name"] = user.display_name if user else m.user_id
    return d


@router.get("/{project_id}/members", response_model=list[MemberResponse])
def list_members(
    project_id: str,
    current_user: UserAccount = Depends(get_current_user),
    db: OrmSession = Depends(get_db),
):
    service = ProjectService(db)
    members = service.list_members(project_id, current_user)
    return [_enrich_member(m, db) for m in members]


@router.post("/{project_id}/members", response_model=MemberResponse)
def add_member(
    project_id: str,
    body: MemberAdd,
    request: Request,
    current_user: UserAccount = Depends(get_current_user),
    db: OrmSession = Depends(get_db),
):
    service = ProjectService(db)
    ip = request.client.host if request.client else None
    m = service.add_member(project_id, body.user_id, body.role, current_user, ip)
    return _enrich_member(m, db)


@router.patch("/{project_id}/members/{user_id}", response_model=MemberResponse)
def update_member_role(
    project_id: str,
    user_id: str,
    body: MemberUpdate,
    request: Request,
    current_user: UserAccount = Depends(get_current_user),
    db: OrmSession = Depends(get_db),
):
    service = ProjectService(db)
    ip = request.client.host if request.client else None
    m = service.update_member_role(project_id, user_id, body.role, current_user, ip)
    return _enrich_member(m, db)


@router.delete("/{project_id}/members/{user_id}")
def remove_member(
    project_id: str,
    user_id: str,
    request: Request,
    current_user: UserAccount = Depends(get_current_user),
    db: OrmSession = Depends(get_db),
):
    service = ProjectService(db)
    ip = request.client.host if request.client else None
    service.remove_member(project_id, user_id, current_user, ip)
    return {"ok": True}


# ── project activities ────────────────────────────────────────

@router.get("/{project_id}/activities", response_model=PaginatedResponse[ActivityResponse])
def project_activities(
    project_id: str,
    page: int = 1,
    per_page: int = 20,
    action: Optional[str] = None,
    actor_id: Optional[str] = None,
    date_from: Optional[str] = None,
    date_to: Optional[str] = None,
    current_user: UserAccount = Depends(get_current_user),
    db: OrmSession = Depends(get_db),
):
    # Check access first
    service = ProjectService(db)
    project = service._get_project(project_id)
    service._check_access(project, current_user)
    items, total = service.list_activities(
        page=page, per_page=per_page, project_id=project_id,
        action=action, actor_id=actor_id, date_from=date_from, date_to=date_to,
    )
    return PaginatedResponse(
        items=[_enrich_activity(a, db) for a in items],
        total=total,
        page=page,
        per_page=per_page,
    )


# ── global activities (admin only) ───────────────────────────

@activity_router.get("/", response_model=PaginatedResponse[ActivityResponse])
def all_activities(
    page: int = 1,
    per_page: int = 20,
    action: Optional[str] = None,
    actor_id: Optional[str] = None,
    date_from: Optional[str] = None,
    date_to: Optional[str] = None,
    admin: UserAccount = Depends(require_admin),
    db: OrmSession = Depends(get_db),
):
    service = ProjectService(db)
    items, total = service.list_activities(
        page=page, per_page=per_page,
        action=action, actor_id=actor_id, date_from=date_from, date_to=date_to,
    )
    return PaginatedResponse(
        items=[_enrich_activity(a, db) for a in items],
        total=total,
        page=page,
        per_page=per_page,
    )


# ── LLM 모델 설정 ──

@router.get("/{project_id}/llm-config")
def get_llm_config(
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """프로젝트별 LLM 모델 설정 조회 (기본값 포함)."""
    from app.models.project import ProjectSettings
    from app.modules.llm.llm_client import PIPELINE_STEPS, AVAILABLE_MODELS, _resolve_model

    ps = db.query(ProjectSettings).filter(ProjectSettings.project_id == project_id).first()
    project_config = {}
    if ps and ps.llm_config_json:
        try:
            project_config = json.loads(ps.llm_config_json)
        except (json.JSONDecodeError, TypeError) as exc:
            logging.getLogger(__name__).warning(
                "llm_config_json parse failed for project %s: %s — 기본 {} 사용", project_id, exc)

    # AVAILABLE_MODELS를 provider별 dict로 변환
    models_by_provider: Dict[str, list] = {"gemini": [], "openai": []}
    _ALIAS_PROVIDER: Dict[str, str] = {}
    for m in AVAILABLE_MODELS:
        provider = m.get("provider", "gemini")
        _ALIAS_PROVIDER[m["alias"]] = provider
        models_by_provider.setdefault(provider, []).append({
            "id": m["alias"],
            "label": m["label"],
            "tier": "",
            "context": "",
        })

    steps = []
    for step_key, step_info in PIPELINE_STEPS.items():
        current_model = _resolve_model(step_key, project_config)
        # provider 는 AVAILABLE_MODELS(alias→provider map)가 SOT — startswith
        # 추론은 gpt-mini(물리=Gemini flash) 같은 역사적 alias 에서 split-brain
        # (router=Gemini, catalog=OpenAI)을 만든다 (Codex NARROW 2026-07-11).
        provider = _ALIAS_PROVIDER.get(
            current_model,
            "openai" if current_model.startswith("gpt") else "gemini",
        )
        steps.append({
            "step": step_key,
            "label": step_info["label"],
            "category": step_info["category"],
            "provider": provider,
            "model": current_model,
            "is_default": step_key not in project_config,
        })

    return {
        "steps": steps,
        "available_models": models_by_provider,
    }


class LLMConfigUpdate(BaseModel):
    config: Dict[str, Dict[str, str]]  # { step_key: { provider, model } }


class GroundingCoordinatesBody(BaseModel):
    """선언 시대·지역 — 비우면 선언을 지운다(모델 추론값으로 돌아간다)."""
    era: Optional[str] = None
    region: Optional[str] = None


def _upsert_project_settings_config(db: OrmSession, project_id: str, mutate) -> Dict[str, Any]:
    """`ProjectSettings.llm_config_json` 을 읽어 `mutate(dict) -> dict` 로 바꿔 저장한다.
    ★한 자리 — LLM 설정 저장과 좌표 선언이 같은 칸을 쓰므로 쓰는 법도 하나여야 한다."""
    from app.models.project import ProjectSettings
    from datetime import datetime, timezone

    ps = db.query(ProjectSettings).filter(ProjectSettings.project_id == project_id).first()
    existing: Dict[str, Any] = {}
    if ps and ps.llm_config_json:
        try:
            existing = json.loads(ps.llm_config_json) or {}
        except json.JSONDecodeError:
            logger.warning("llm_config_json parse failed for project %s — 빈 설정에서 다시 쓴다", project_id)
    new_cfg = mutate(existing)
    now = datetime.now(timezone.utc).isoformat()
    if not ps:
        import uuid
        ps = ProjectSettings(id=str(uuid.uuid4()), project_id=project_id,
                             llm_config_json=json.dumps(new_cfg, ensure_ascii=False), updated_at=now)
        db.add(ps)
    else:
        ps.llm_config_json = json.dumps(new_cfg, ensure_ascii=False)
        ps.updated_at = now
    db.commit()
    return new_cfg


@router.get("/{project_id}/grounding-coordinates")
def get_grounding_coordinates(
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
):
    """선언된 고증 좌표(시대·지역). 없으면 빈 값 — 파이프라인은 모델 추론값을 쓴다."""
    from app.modules.pipeline.grounding_coordinates import (
        DECLARED_ERA_KEY, DECLARED_REGION_KEY, declared_coordinates)
    from app.services.analysis_dispatch_service import load_project_llm_config

    got = declared_coordinates(load_project_llm_config(db, project_id))
    return {"era": got.get("era", ""), "region": got.get("region", ""),
            "keys": {"era": DECLARED_ERA_KEY, "region": DECLARED_REGION_KEY}}


@router.put("/{project_id}/grounding-coordinates")
def update_grounding_coordinates(
    body: GroundingCoordinatesBody,
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """고증 좌표 선언 — `visual_world_rules` 가 **그 글자 그대로** 시대·지역으로 쓴다
    (Codex BLOCK 2026-09-02: 선언을 읽는 쪽만 있고 **쓰는 production 입구가 없었다**)."""
    from app.modules.pipeline.grounding_coordinates import (
        declared_coordinates, with_declared_coordinates)

    # ★★owner/admin 만 — member 는 읽기다 (Codex BLOCK 2026-09-02 밤). DB 는 안 건드린다.
    require_project_write(project_id, db, current_user)
    cfg = _upsert_project_settings_config(
        db, project_id, lambda existing: with_declared_coordinates(
            existing, era=body.era, region=body.region))
    got = declared_coordinates(cfg)
    return {"ok": True, "era": got.get("era", ""), "region": got.get("region", "")}


@router.put("/{project_id}/llm-config")
def update_llm_config(
    body: LLMConfigUpdate,
    project_id: str = Depends(verify_project_access),
    db: OrmSession = Depends(get_db),
    current_user: UserAccount = Depends(get_current_user),
):
    """프로젝트별 LLM 모델 설정 저장. ★스칼라 설정 키(선언 좌표 등)는 남긴다."""
    from app.modules.pipeline.grounding_coordinates import merge_step_config

    _upsert_project_settings_config(
        db, project_id, lambda existing: merge_step_config(existing, body.config))
    return {"ok": True, "message": "LLM 설정이 저장되었습니다"}
