#!/usr/bin/env python3
from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse, JSONResponse
from pathlib import Path
from pydantic import BaseModel
from typing import List, Optional
import uvicorn, uuid, json, time, hashlib, asyncio
from datetime import datetime
import db

app = FastAPI()

from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request

class NoCacheMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        response = await call_next(request)
        if request.url.path.startswith('/p/'):
            response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
            response.headers['Pragma'] = 'no-cache'
            response.headers['Expires'] = '0'
        return response

app.add_middleware(NoCacheMiddleware)
BASE = Path('/home/jooyoung/.openclaw/workspace-260312/orchestration')
PROJECTS = BASE / 'projects'
SERVING = BASE / 'serving'
REQUESTS = BASE / 'requests'
REQUESTS.mkdir(exist_ok=True)

# ============================================================
# Build Hash & Adapter Versions
# ============================================================
def compute_build_hash():
    """shared/ 디렉토리 파일들의 MD5 해시 계산"""
    h = hashlib.md5()
    shared_dir = BASE.parent / 'projects' / 'shared'
    if shared_dir.exists():
        for f in sorted(shared_dir.rglob('*')):
            if f.is_file():
                try:
                    h.update(f.read_bytes())
                except:
                    pass
    return h.hexdigest()[:8]

def collect_adapter_versions():
    """어댑터별 버전 수집"""
    versions = {}
    adapters_dir = BASE.parent / 'projects' / 'adapters'
    if adapters_dir.exists():
        for d in adapters_dir.iterdir():
            if d.is_dir():
                meta = d / 'adapter.json'
                if meta.exists():
                    try:
                        data = json.loads(meta.read_text())
                        versions[d.name] = data.get('version', '0.0')
                    except:
                        pass
    return versions

BUILD_HASH = compute_build_hash()
ADAPTER_VERSIONS = collect_adapter_versions()
SERVER_START_TIME = time.time()

# Request counter for periodic cleanup
_request_counter = 0

# ============================================================
# Startup Event
# ============================================================
@app.on_event("startup")
async def startup_event():
    """서버 시작 시 DB 초기화"""
    db_path = BASE / 'data' / 'battle.db'
    db.init_db(str(db_path))
    db.cleanup_stale_sessions(30)
    print(f"🗄️  Database initialized: {db_path}")
    print(f"🔨 Build hash: {BUILD_HASH}")
    print(f"📦 Adapters: {list(ADAPTER_VERSIONS.keys())}")

class GenerateRequest(BaseModel):
    genre: str
    adapter: str
    volume: str
    tones: List[str] = []
    synopsis: str = ''
    timestamp: Optional[str] = None

from fastapi.responses import RedirectResponse

@app.get('/')
def index():
    return RedirectResponse('/p/factory/')

@app.get('/api/games')
def list_games():
    games = []
    seen = set()
    for d in [PROJECTS, SERVING]:
        if not d.exists(): continue
        for p in d.iterdir():
            real = p.resolve() if p.is_symlink() else p
            if real.is_dir() and (real / 'index.html').exists() and p.name not in seen:
                seen.add(p.name)
                games.append({'name': p.name, 'url': f'/p/{p.name}/'})
    return games

@app.get('/health')
async def health_check_short():
    return {'status': 'ok'}

@app.get('/api/health')
def health():
    return {'status': 'ok'}

@app.get('/api/adapters')
def list_adapters():
    registry_path = BASE.parent / 'projects' / 'shared' / 'adapter-registry.json'
    if registry_path.exists():
        return json.loads(registry_path.read_text())
    return {"version": "1.0", "adapters": {}}

# ============================================================
# Battle Result API with User System (DB-backed)
# ============================================================

@app.post('/api/battle/{battle_id}')
async def post_battle_result(battle_id: str, request: Request):
    """어댑터가 전투 결과를 보고 (확장 필드 수용)"""
    global _request_counter
    _request_counter += 1
    
    body = await request.json()
    body["battleId"] = battle_id  # battleId 보장
    
    # DB에 저장 (run_in_executor로 동기 함수 호출)
    loop = asyncio.get_event_loop()
    result = await loop.run_in_executor(None, db.insert_battle, body)
    
    # 주기적 cleanup (매 100번째 요청)
    if _request_counter % 100 == 0:
        await loop.run_in_executor(None, db.cleanup_stale_sessions, 30)
    
    return {
        "status": "ok",
        "expGained": result.get("expGained", 0),
        "level": result.get("userLevel", 1)
    }

@app.get('/api/battle/{battle_id}')
async def get_battle_result(battle_id: str):
    """테스터/Playwright가 결과 조회 (확장 필드 포함)"""
    loop = asyncio.get_event_loop()
    battle = await loop.run_in_executor(None, db.get_battle, battle_id)
    
    if battle:
        return {"status": "ready", "data": battle}
    return {"status": "pending"}

@app.get('/api/user/{user_id}/history')
async def get_user_history(user_id: str, limit: int = 50):
    """유저의 전투 이력 조회"""
    loop = asyncio.get_event_loop()
    battles = await loop.run_in_executor(None, db.get_user_history, user_id, limit)
    
    return {
        "userId": user_id,
        "battles": battles
    }

@app.get('/api/user/{user_id}/stats')
async def get_user_stats(user_id: str):
    """유저 통계 (기존 응답 구조 유지)"""
    loop = asyncio.get_event_loop()
    stats = await loop.run_in_executor(None, db.get_user_stats, user_id)
    return stats

# ============================================================
# New Endpoints
# ============================================================

@app.post('/api/user/register')
async def register_user(request: Request):
    """유저 등록/업데이트"""
    body = await request.json()
    user_id = body.get('userId')
    device_hash = body.get('deviceHash')
    display_name = body.get('displayName')
    settings = body.get('settings')
    
    loop = asyncio.get_event_loop()
    result = await loop.run_in_executor(
        None, db.upsert_user, user_id, device_hash, display_name, settings
    )
    
    return {
        'status': 'ok',
        'userId': result['userId'],
        'level': result['level'],
        'exp': result['exp']
    }

@app.get('/api/version')
async def get_version():
    """빌드 해시 및 어댑터 버전"""
    return {
        'build': BUILD_HASH,
        'timestamp': SERVER_START_TIME,
        'schema': 1,
        'adapters': ADAPTER_VERSIONS
    }

@app.post('/api/session')
async def create_session(request: Request):
    """게임 세션 생성"""
    body = await request.json()
    user_id = body.get('userId')
    game_id = body.get('gameId')
    client_build = body.get('clientBuild')
    
    loop = asyncio.get_event_loop()
    session_id = await loop.run_in_executor(
        None, db.create_session, user_id, game_id, client_build
    )
    
    return {'sessionId': session_id}

@app.post('/api/session/{session_id}/end')
async def end_session(session_id: str, request: Request):
    """세션 종료"""
    body = await request.json()
    end_reason = body.get('endReason', 'unknown')
    ending_id = body.get('endingId')
    drop_node_id = body.get('dropNodeId')
    
    loop = asyncio.get_event_loop()
    result = await loop.run_in_executor(
        None, db.end_session, session_id, end_reason, ending_id, drop_node_id
    )
    
    return result

@app.post('/api/session/{session_id}/update')
async def update_session(session_id: str, request: Request):
    """세션 업데이트"""
    body = await request.json()
    
    loop = asyncio.get_event_loop()
    # run_in_executor는 **kwargs를 직접 전달할 수 없으므로 lambda 사용
    result = await loop.run_in_executor(None, lambda: db.update_session(session_id, **body))
    
    return result

@app.post('/api/event')
async def log_event(request: Request):
    """범용 이벤트 로그"""
    body = await request.json()
    user_id = body.get('userId')
    session_id = body.get('sessionId')
    game_id = body.get('gameId')
    event_type = body.get('eventType')
    event_data = body.get('eventData', {})
    
    loop = asyncio.get_event_loop()
    event_id = await loop.run_in_executor(
        None, db.insert_event, user_id, session_id, game_id, event_type, event_data
    )
    
    return {'status': 'ok', 'eventId': event_id}

@app.get('/api/game/{game_id}/stats')
async def get_game_stats(game_id: str):
    """게임 통계"""
    loop = asyncio.get_event_loop()
    stats = await loop.run_in_executor(None, db.get_game_stats, game_id)
    return stats

# ============================================================
# Session Restore + Analytics API (Phase 1)
# ============================================================

@app.get('/api/user/{user_id}/sessions')
async def get_user_sessions_endpoint(user_id: str, gameId: str = None, status: str = None, limit: int = 20):
    """유저의 세션 목록 조회"""
    loop = asyncio.get_event_loop()
    sessions = await loop.run_in_executor(
        None, lambda: db.get_user_sessions(user_id, gameId, status, limit)
    )
    return {'sessions': sessions, 'count': len(sessions)}

@app.get('/api/session/{session_id}')
async def get_session_endpoint(session_id: str):
    """세션 상세 조회 (save_state 포함)"""
    loop = asyncio.get_event_loop()
    session = await loop.run_in_executor(None, db.get_session, session_id)
    
    if session:
        return session
    
    from fastapi.responses import JSONResponse
    return JSONResponse({'error': 'session not found'}, status_code=404)

@app.get('/api/analytics/overview')
async def analytics_overview():
    """글로벌 통계"""
    loop = asyncio.get_event_loop()
    stats = await loop.run_in_executor(None, db.get_analytics_overview)
    return stats

@app.get('/api/analytics/funnel')
async def analytics_funnel(gameId: str):
    """게임별 퍼널 — battleCount별 세션 수"""
    loop = asyncio.get_event_loop()
    funnel = await loop.run_in_executor(None, db.get_analytics_funnel, gameId)
    return funnel

@app.get('/api/analytics/adapters')
async def analytics_adapters():
    """어댑터별 통계"""
    loop = asyncio.get_event_loop()
    stats = await loop.run_in_executor(None, db.get_analytics_adapters)
    return stats

@app.get('/api/analytics/timeline')
async def analytics_timeline(days: int = 30):
    """일별 세션/전투 수"""
    loop = asyncio.get_event_loop()
    timeline = await loop.run_in_executor(None, db.get_analytics_timeline, days)
    return timeline

# ============================================================
# Error Cases API
# ============================================================

@app.post('/api/error')
async def post_error(request: Request):
    """에러 케이스 기록"""
    body = await request.json()
    loop = asyncio.get_event_loop()
    result = await loop.run_in_executor(None, lambda: db.insert_error(body))
    return result

@app.post('/api/error/{error_id}/resolve')
async def resolve_error(error_id: int, request: Request):
    """에러 해결 기록"""
    body = await request.json()
    loop = asyncio.get_event_loop()
    result = await loop.run_in_executor(None, lambda: db.resolve_error(error_id, body))
    return result

@app.get('/api/errors/search')
async def search_errors(
    error_type: str = None, source: str = None,
    keyword: str = None, resolved_only: bool = True, limit: int = 5
):
    """유사 에러 검색"""
    loop = asyncio.get_event_loop()
    results = await loop.run_in_executor(
        None, lambda: db.search_errors(error_type, source, keyword, resolved_only, limit)
    )
    return {'results': results, 'count': len(results)}

@app.get('/api/errors/stats')
async def error_stats():
    """에러 통계"""
    loop = asyncio.get_event_loop()
    stats = await loop.run_in_executor(None, db.get_error_stats)
    return stats

@app.post('/api/generate')
def generate(req: GenerateRequest):
    rid = uuid.uuid4().hex[:8]
    data = {
        'id': rid,
        'genre': req.genre,
        'adapter': req.adapter,
        'volume': req.volume,
        'tones': req.tones,
        'synopsis': req.synopsis,
        'status': 'pending',
        'createdAt': req.timestamp or datetime.now().isoformat(),
        'projectId': None
    }
    (REQUESTS / f'{rid}.json').write_text(json.dumps(data, ensure_ascii=False, indent=2))
    return {'requestId': rid, 'status': 'accepted'}

@app.get('/api/requests')
def list_requests():
    results = []
    for f in REQUESTS.glob('*.json'):
        try: results.append(json.loads(f.read_text()))
        except: pass
    results.sort(key=lambda x: x.get('createdAt', ''), reverse=True)
    return results

@app.get('/api/requests/{rid}')
def get_request(rid: str):
    p = REQUESTS / f'{rid}.json'
    if not p.exists():
        from fastapi.responses import JSONResponse
        return JSONResponse({'error': 'not found'}, status_code=404)
    return json.loads(p.read_text())

# ============================================================
# PlayLog API
# ============================================================
PLAYLOGS = BASE / 'playlogs'

@app.post('/api/playlog')
async def save_playlog(request: Request):
    """플레이 로그 저장 (파일 + DB 이중 기록)"""
    try:
        data = await request.json()
        version = data.get('meta', {}).get('version', 'unknown')
        session_id = data.get('meta', {}).get('sessionId', 'unknown')
        user_id = data.get('meta', {}).get('userId')
        game_id = data.get('meta', {}).get('gameId')
        
        # 1. 기존 파일 기반 저장
        log_dir = PLAYLOGS / version
        log_dir.mkdir(parents=True, exist_ok=True)
        
        log_file = log_dir / f'{session_id}.json'
        
        # 기존 파일이 있으면 append (logs 배열 병합), 없으면 새로 생성
        if log_file.exists():
            try:
                existing_data = json.loads(log_file.read_text())
                existing_data['logs'].extend(data.get('logs', []))
                log_file.write_text(json.dumps(existing_data, ensure_ascii=False, indent=2))
            except:
                log_file.write_text(json.dumps(data, ensure_ascii=False, indent=2))
        else:
            log_file.write_text(json.dumps(data, ensure_ascii=False, indent=2))
        
        # 로테이션: 버전 디렉토리 내 10개 초과 시 오래된 순 삭제
        cleanup_old_sessions(log_dir, max_sessions=10)
        
        # 2. DB events 테이블에도 INSERT (마이그레이션 기간 이중 기록)
        loop = asyncio.get_event_loop()
        for log_entry in data.get('logs', []):
            event_type = log_entry.get('type', 'unknown')
            event_data = log_entry.get('data', {})
            
            await loop.run_in_executor(
                None, db.insert_event, user_id, session_id, game_id, event_type, event_data
            )
        
        return {'status': 'ok', 'sessionId': session_id}
    except Exception as e:
        return JSONResponse({'status': 'error', 'message': str(e)}, status_code=500)

@app.delete('/api/playlog/{version}')
async def clear_playlogs(version: str):
    try:
        log_dir = PLAYLOGS / version
        if log_dir.exists():
            import shutil
            shutil.rmtree(log_dir)
        return {'status': 'ok', 'message': f'Cleared playlogs for version {version}'}
    except Exception as e:
        return JSONResponse({'status': 'error', 'message': str(e)}, status_code=500)

@app.get('/api/playlog/{version}')
async def list_playlogs(version: str):
    try:
        log_dir = PLAYLOGS / version
        if not log_dir.exists():
            return {'sessions': []}
        
        sessions = []
        for log_file in log_dir.glob('*.json'):
            try:
                data = json.loads(log_file.read_text())
                sessions.append({
                    'sessionId': data.get('meta', {}).get('sessionId'),
                    'startedAt': data.get('meta', {}).get('startedAt'),
                    'logCount': len(data.get('logs', []))
                })
            except:
                pass
        
        sessions.sort(key=lambda x: x.get('startedAt', ''), reverse=True)
        return {'sessions': sessions}
    except Exception as e:
        return JSONResponse({'status': 'error', 'message': str(e)}, status_code=500)

def cleanup_old_sessions(log_dir: Path, max_sessions: int = 10):
    try:
        session_files = sorted(log_dir.glob('*.json'), key=lambda f: f.stat().st_mtime)
        while len(session_files) > max_sessions:
            oldest = session_files.pop(0)
            oldest.unlink()
    except Exception as e:
        print(f'Failed to cleanup old sessions: {e}')

# /shared/ 경로 서빙 (battle-shim.js 등)
app.mount('/themes', StaticFiles(directory=str(BASE.parent / 'projects' / 'shared' / 'themes')), name='theme_assets')
app.mount('/shared', StaticFiles(directory=str(BASE.parent / 'projects' / 'shared')), name='shared_assets')

# /adapters/ 경로 서빙 (Adapter Registry canonical paths)
ADAPTERS_DIR = BASE.parent / 'projects' / 'adapters'
if ADAPTERS_DIR.exists():
    for adapter_dir in ADAPTERS_DIR.iterdir():
        if adapter_dir.is_dir() and (adapter_dir / 'index.html').exists():
            try:
                app.mount(f'/adapters/{adapter_dir.name}', StaticFiles(directory=str(adapter_dir), html=True), name=f'adapter_{adapter_dir.name}')
            except: pass

from fastapi.responses import FileResponse
@app.get('/assets/shared/{path:path}')
async def serve_shared(path: str):
    file_path = BASE.parent / 'shared' / 'dist' / path
    if file_path.exists():
        return FileResponse(file_path, media_type='application/javascript')
    return JSONResponse({'error': 'not found'}, status_code=404)

for d in [PROJECTS, SERVING]:
    if not d.exists(): continue
    for p in d.iterdir():
        real = p.resolve() if p.is_symlink() else p
        if real.is_dir() and (real / 'index.html').exists():
            try:
                app.mount(f'/p/{p.name}', StaticFiles(directory=str(real), html=True), name=f's_{p.name}')
            except: pass

if __name__ == '__main__':
    uvicorn.run(app, host='0.0.0.0', port=8600)
