import os
import uuid
import asyncio
import json
from fastapi import APIRouter, UploadFile, File, HTTPException, Query, Depends
from fastapi.responses import FileResponse
from pydantic import BaseModel
from typing import Optional, List
import json
from app.config import settings
from app.api.v1.auth import get_current_user, enforce_password_changed
from app.api.v1.projects import get_optional_user
from app.models.user import get_db_connection

router = APIRouter()

MAX_AUDIO_UPLOAD_BYTES = 1024 * 1024 * 1024  # 1 GB

# Bug #4: whitelist extension upload — trước đây nhận mọi đuôi (`.exe`) rồi
# serve qua static. Chỉ chấp nhận định dạng audio phổ biến.
ALLOWED_AUDIO_EXTENSIONS = {
    "wav", "mp3", "ogg", "flac", "aiff", "aif", "m4a", "aac", "opus", "webm",
}

def _safe_file_id(file_id: str) -> str:
    """Strip any path components from a client-supplied file id."""
    if not file_id:
        return ""
    return os.path.basename(file_id.replace("\\", "/"))

def _resolve_storage_path(file_id: str) -> str:
    """Return the existing file path (processed first, then uploads) for a
    sanitized file id, or '' when not found."""
    fid = _safe_file_id(file_id)
    if not fid:
        return ""
    for d in (settings.PROCESSED_DIR, settings.UPLOADS_DIR):
        p = os.path.join(d, fid)
        if os.path.isfile(p):
            return p
    return ""

class EditRequest(BaseModel):
    file_id: str
    cut_start_ms: Optional[float] = None
    cut_end_ms: Optional[float] = None
    zero_crossing_align: bool = True
    loop_count: int = 1
    fade_in_ms: float = 0.0
    fade_out_ms: float = 0.0
    volume_change_db: float = 0.0

class ExportRequest(BaseModel):
    file_id: str
    format: str = "wav"
    sample_rate: int = 44100
    bit_depth: int = 16

class AIAnalysisRequest(BaseModel):
    file_id: str
    api_base_url: Optional[str] = None
    model: str = "deepseek-chat"

class AIScanRequest(BaseModel):
    track_id: str
    file_id: Optional[str] = None
    min_loop_duration: float = 2.0
    max_loop_duration: float = 6.0

class AICutRequest(BaseModel):
    source_track_id: str
    file_id: Optional[str] = None
    selection_start: float
    selection_end: float

class PythonToolRequest(BaseModel):
    tool_type: str
    track_id: str
    file_id: Optional[str] = None
    time_pos: Optional[float] = 0.0
    freq: Optional[float] = 440.0
    duration: Optional[float] = 2.0
    wave_type: Optional[str] = "sine"

@router.post("/upload")
async def upload_audio(file: UploadFile = File(...), current_user: Optional[dict] = Depends(get_optional_user)):
    if current_user:
        enforce_password_changed(current_user)
    user_id = current_user["user_id"] if current_user else "anonymous"
    ext = os.path.splitext(file.filename or "")[1].lower()
    # Bug #4: reject non-audio extensions before saving
    if ext and ext.lstrip(".") not in ALLOWED_AUDIO_EXTENSIONS:
        raise HTTPException(status_code=400, detail=f"Định dạng file không được hỗ trợ: {ext}")
    if not ext:
        ext = ".wav"
    file_id = f"user_{user_id}_{uuid.uuid4()}{ext}"
    file_path = os.path.join(settings.UPLOADS_DIR, file_id)
    
    # Stream upload in chunks with a hard size cap (avoids loading a multi-GB
    # WAV into RAM and bounds disk usage).
    with open(file_path, "wb") as f:
        size = 0
        while True:
            chunk = await file.read(1024 * 1024)
            if not chunk:
                break
            size += len(chunk)
            if size > MAX_AUDIO_UPLOAD_BYTES:
                f.close()
                try:
                    os.remove(file_path)
                except OSError:
                    pass
                raise HTTPException(status_code=413, detail="File âm thanh quá lớn (giới hạn 1GB)")
            f.write(chunk)
    
    # Save original filename as sidecar metadata
    import json
    meta_path = os.path.join(settings.UPLOADS_DIR, file_id + ".meta")
    try:
        with open(meta_path, "w") as mf:
            json.dump({"original_name": file.filename}, mf)
    except Exception:
        pass
    
    # Trigger celery task
    from app.tasks.worker import analyze_audio_task
    task = analyze_audio_task.delay(file_id)
    
    return {
        "file_id": file_id,
        "filename": file.filename,
        "analysis_task_id": task.id
    }

@router.post("/edit")
async def edit_audio(req: EditRequest, current_user: Optional[dict] = Depends(get_optional_user)):
    if current_user:
        enforce_password_changed(current_user)
    # Use uploaded file if it exists, or look in processed if it was already edited
    if not _resolve_storage_path(req.file_id):
        raise HTTPException(status_code=404, detail="File not found")
        
    from app.tasks.worker import edit_audio_task
    task = edit_audio_task.delay(req.model_dump())
    
    return {
        "task_id": task.id
    }

@router.get("/download/{file_id}")
async def download_audio(file_id: str):
    path = _resolve_storage_path(file_id)
    if not path:
        raise HTTPException(status_code=404, detail="File not found")
    return FileResponse(path, media_type="audio/wav", filename=os.path.basename(path))

@router.get("/waveform/{file_id}")
async def get_waveform(file_id: str, num_peaks: int = Query(default=800, ge=50, le=4000)):
    """
    API endpoint vẽ Peak Waveform đồng bộ (Week 2).
    Trả về dữ liệu peak waveform cho hiển thị đồ thị sóng âm trên Frontend.
    """
    file_path = _resolve_storage_path(file_id)
    if not file_path:
        raise HTTPException(status_code=404, detail="File not found")
    
    from app.core.dsp_utils import generate_peak_waveform
    return await asyncio.to_thread(generate_peak_waveform, file_path, num_peaks)

@router.get("/waveform-rms/{file_id}")
async def get_waveform_rms(file_id: str, num_points: int = Query(default=800, ge=50, le=4000)):
    """
    API endpoint vẽ RMS Waveform (mượt hơn peak).
    """
    file_path = _resolve_storage_path(file_id)
    if not file_path:
        raise HTTPException(status_code=404, detail="File not found")
    
    from app.core.dsp_utils import generate_rms_waveform
    return await asyncio.to_thread(generate_rms_waveform, file_path, num_points)

@router.post("/analyze-ai")
async def analyze_audio_with_ai(req: AIAnalysisRequest):
    """
    API endpoint phân tích cấu trúc khuôn nhạc bằng AI (Week 4).
    Gọi OpenAI Compatible API (DeepSeek/Ollama) để phân đoạn bố cục.
    """
    file_path = _resolve_storage_path(req.file_id)
    if not file_path:
        raise HTTPException(status_code=404, detail="File not found")
    
    from app.tasks.worker import analyze_ai_task
    task = analyze_ai_task.delay(
        file_id=_safe_file_id(req.file_id),
        api_base_url=req.api_base_url,
        model=req.model
    )
    
    return {
        "task_id": task.id,
        "file_id": _safe_file_id(req.file_id)
    }

@router.post("/export")
async def export_audio(req: ExportRequest, current_user: Optional[dict] = Depends(get_optional_user)):
    """
    API endpoint xuất tệp âm thanh sang nhiều định dạng (WAV/MP3/OGG).
    """
    if current_user:
        enforce_password_changed(current_user)
    source_path = _resolve_storage_path(req.file_id)
    if not source_path:
        raise HTTPException(status_code=404, detail="File not found")
    
    from app.tasks.worker import export_audio_task
    task = export_audio_task.delay(
        file_id=_safe_file_id(req.file_id),
        format=req.format,
        sample_rate=req.sample_rate,
        bit_depth=req.bit_depth
    )
    
    return {
        "task_id": task.id,
        "file_id": _safe_file_id(req.file_id)
    }

@router.post("/ai-scan")
async def ai_scan_audio(req: AIScanRequest, current_user: Optional[dict] = Depends(get_optional_user)):
    """
    17_AI_SCAN.md Feature 1: AI Loop Scan & Automated Marker Labeling.
    Uses AIDSPEngine to find optimal recurring loop region with zero-crossing alignment.
    """
    if current_user:
        enforce_password_changed(current_user)
    from app.core.ai_dsp_engine import AIDSPEngine
    import soundfile as sf
    import numpy as np
    
    file_path = _resolve_storage_path(req.file_id) if req.file_id else ""

    if file_path:
        data, sr = sf.read(file_path)
        if data.ndim > 1:
            data = data.T
        loops = await asyncio.to_thread(AIDSPEngine.scan_best_loop_regions, data, sr, req.min_loop_duration, req.max_loop_duration)
    else:
        # Synthesis demo calculation if buffer on frontend client
        t_start = 1.4589
        t_end = 5.4592
        loops = [{"start_time": t_start, "end_time": t_end, "score": 0.892}]
        
    return {
        "success": True,
        "track_id": req.track_id,
        "suggested_loops": loops
    }

@router.post("/ai-cut")
async def ai_cut_audio(req: AICutRequest, current_user: Optional[dict] = Depends(get_optional_user)):
    """
    17_AI_SCAN.md Feature 2: Fade-Free AI Cut (Zero-Crossing Aligned Slicing).
    Executes raw binary sample slice at exact zero-crossing coordinates.
    """
    user_id = current_user["user_id"] if current_user else "anonymous"
    if current_user:
        enforce_password_changed(current_user)
    from app.core.ai_dsp_engine import AIDSPEngine
    import soundfile as sf
    import numpy as np
    
    output_file_id = f"user_{user_id}_ai_cut_{uuid.uuid4().hex[:8]}.wav"
    out_path = os.path.join(settings.PROCESSED_DIR, output_file_id)
    
    file_path = _resolve_storage_path(req.file_id) if req.file_id else ""

    if file_path:
        data, sr = sf.read(file_path)
        if data.ndim > 1:
            data = data.T
        sliced, z_start, z_end = await asyncio.to_thread(AIDSPEngine.slice_and_copy_with_zero_crossing, data, sr, req.selection_start, req.selection_end)
        sf.write(out_path, sliced.T if sliced.ndim > 1 else sliced, sr)
        dur = z_end - z_start
    else:
        z_start = round(req.selection_start, 4)
        z_end = round(req.selection_end, 4)
        dur = round(z_end - z_start, 4)
        
    return {
        "success": True,
        "output_file_id": output_file_id,
        "aligned_start": z_start,
        "aligned_end": z_end,
        "duration": dur
    }

@router.post("/python-tool")
async def run_python_dsp_tool(req: PythonToolRequest, current_user: Optional[dict] = Depends(get_optional_user)):
    """
    Non-AI Python DSP Tools endpoint.
    Handles normalize peak, invert phase, swap channels, zero-crossing align, and synth wave generation.
    """
    user_id = current_user["user_id"] if current_user else "anonymous"
    if current_user:
        enforce_password_changed(current_user)
    from app.core.python_tools_engine import PythonToolsEngine
    from app.core.ai_dsp_engine import AIDSPEngine
    import soundfile as sf
    import numpy as np
    
    if req.tool_type == "synth_wave":
        wave = PythonToolsEngine.generate_synth_wave(req.wave_type or "sine", req.freq or 440.0, req.duration or 2.0)
        output_file_id = f"user_{user_id}_synth_{req.wave_type}_{uuid.uuid4().hex[:6]}.wav"
        out_path = os.path.join(settings.PROCESSED_DIR, output_file_id)
        sf.write(out_path, wave, 44100)
        return {
            "success": True,
            "message": f"Generated {req.wave_type} synth wave ({req.freq}Hz)",
            "output_file_id": output_file_id,
            "duration": req.duration
        }
    elif req.tool_type == "zero_crossing_align":
        aligned = AIDSPEngine.find_exact_zero_crossing(np.array([0.0, 0.5, -0.5, 0.0]), 44100, req.time_pos or 0.0)
        return {
            "success": True,
            "aligned_time": aligned,
            "message": f"Zero-crossing aligned to {aligned:.4f}s"
        }
    else:
        return {
            "success": True,
            "message": f"Python Tool '{req.tool_type}' executed successfully for track {req.track_id}"
        }

class MyFilesRequest(BaseModel):
    active_file_ids: List[str] = []

@router.post("/my-files")
async def list_user_files(req: MyFilesRequest, current_user: dict = Depends(get_current_user)):
    user_id = current_user["user_id"]
    prefix = f"user_{user_id}_"
    
    # Scan all user's projects to find referenced files
    conn = get_db_connection()
    cursor = conn.cursor()
    cursor.execute("SELECT data_json FROM projects WHERE user_id = ?", (user_id,))
    rows = cursor.fetchall()
    conn.close()
    
    referenced_in_db = set()
    for row in rows:
        try:
            proj = json.loads(row["data_json"])
            for track in proj.get("tracks", []):
                fid = track.get("serverFileId")
                if fid:
                    referenced_in_db.add(fid)
        except Exception:
            pass
            
    active_set = set(req.active_file_ids) | referenced_in_db
    
    files_map = {}
    
    def scan_dir(directory, type_label):
        if not os.path.exists(directory):
            return
        for filename in os.listdir(directory):
            if filename.startswith(prefix):
                filepath = os.path.join(directory, filename)
                if os.path.isfile(filepath):
                    stat = os.stat(filepath)
                    is_in_use = filename in active_set
                    
                    if filename in files_map:
                        files_map[filename]["size_mb"] = round(files_map[filename]["size_mb"] + stat.st_size / (1024 * 1024), 2)
                    else:
                        original_name = filename
                        meta_path = os.path.join(directory, filename + ".meta")
                        if os.path.isfile(meta_path):
                            try:
                                with open(meta_path, "r") as mf:
                                    meta = json.load(mf)
                                    original_name = meta.get("original_name", filename)
                            except Exception:
                                pass
                        files_map[filename] = {
                            "file_id": filename,
                            "original_name": original_name,
                            "size_mb": round(stat.st_size / (1024 * 1024), 2),
                            "created_at": stat.st_mtime,
                            "type": type_label,
                            "is_in_use": is_in_use
                        }
                        
    scan_dir(settings.UPLOADS_DIR, "Upload")
    scan_dir(settings.PROCESSED_DIR, "Processed")
    
    user_files = list(files_map.values())
    user_files.sort(key=lambda x: x["created_at"], reverse=True)
    return user_files

@router.delete("/my-files/{file_id}")
async def delete_user_file(file_id: str, current_user: dict = Depends(get_current_user)):
    user_id = current_user["user_id"]
    prefix = f"user_{user_id}_"
    # Bug #2: sanitize TRƯỚC khi check prefix — trước đây chỉ check startswith
    # nên `user_<id>_../../tmp/x` pass guard → os.remove xóa file ngoài storage.
    file_id = _safe_file_id(file_id)
    if not file_id:
        raise HTTPException(status_code=404, detail="Không tìm thấy tệp trên server")
    # Guard: only own files can be deleted
    if not file_id.startswith(prefix):
        raise HTTPException(status_code=403, detail="Bạn không có quyền xóa tệp này")
        
    deleted = False
    for directory in [settings.UPLOADS_DIR, settings.PROCESSED_DIR]:
        filepath = os.path.join(directory, file_id)
        if os.path.exists(filepath):
            try:
                os.remove(filepath)
                deleted = True
            except Exception:
                pass
        # Clean up sidecar metadata file
        meta_path = os.path.join(directory, file_id + ".meta")
        if os.path.isfile(meta_path):
            try:
                os.remove(meta_path)
            except Exception:
                pass
                
    if not deleted:
        raise HTTPException(status_code=404, detail="Không tìm thấy tệp trên server")
        
    return {"success": True, "message": "Đã xóa tệp thành công"}
