# app/main.py
"""
FastAPI backend для Blueprint QC Service.

API Endpoints:
- POST /api/projects/upload       — загрузка PDF
- GET  /api/projects              — список проектов
- GET  /api/projects/{id}         — детали проекта
- GET  /api/projects/{id}/issues  — замечания проекта
- POST /api/feedback              — отметить замечание (TP/FP)
- GET  /api/feedback/stats        — статистика feedback
- GET  /api/training/data         — экспорт данных для обучения
- GET  /api/stats                 — общая статистика
- GET  /viewer/{project_id}/{page} — HTML viewer (серверный, без file://)
"""

import os
import sys
import re
import json
import shutil
from pathlib import Path
from typing import List, Optional
from datetime import datetime

from fastapi import FastAPI, File, UploadFile, Depends, HTTPException, Query
from fastapi.responses import HTMLResponse, FileResponse, JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from sqlalchemy.orm import Session

# Добавить пути
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
sys.path.insert(0, str(Path(__file__).parent))

from app.database import engine, get_db, Base
from app import models, schemas, crud, processing

# Создать таблицы
Base.metadata.create_all(bind=engine)

# Создать папку static если нет
STATIC_DIR = Path(__file__).parent.parent / "static"
STATIC_DIR.mkdir(exist_ok=True)

app = FastAPI(
    title="Blueprint QC API",
    description="Сервис автоматической проверки чертежей и сбора данных для обучения",
    version="0.1.0"
)

# CORS
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Пути
# Абсолютные пути (backend запускается из разных директорий)
BASE_DIR = Path(__file__).parent.parent  # backend/
UPLOAD_DIR = BASE_DIR / "uploads"
OUTPUT_DIR = BASE_DIR / "outputs"
UPLOAD_DIR.mkdir(exist_ok=True)
OUTPUT_DIR.mkdir(exist_ok=True)


# ==================== PROJECTS ====================

@app.post("/api/projects/upload", response_model=schemas.ProjectResponse)
async def upload_pdf(
    file: UploadFile = File(...),
    name: Optional[str] = None,
    db: Session = Depends(get_db)
):
    """Загрузка PDF файла. Запускает фоновую обработку."""
    if not file.filename.endswith('.pdf'):
        raise HTTPException(400, "Only PDF files allowed")
    
    # Сохранить файл
    safe_name = Path(file.filename).name
    pdf_path = UPLOAD_DIR / f"{datetime.now().strftime('%Y%m%d_%H%M%S')}_{safe_name}"
    with open(pdf_path, "wb") as f:
        shutil.copyfileobj(file.file, f)
    
    # Создать проект в БД (статус = uploaded, анализ запускается вручную)
    project = crud.create_project(db, pdf_filename=safe_name, name=name or safe_name)
    
    # НЕ запускаем обработку автоматически — пользователь жмет "Анализировать"
    # вручную, когда готов
    
    return project


@app.get("/api/projects", response_model=List[schemas.ProjectDetail])
def list_projects(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
    """Список всех проектов со страницами и замечаниями."""
    return crud.get_projects(db, skip=skip, limit=limit)


@app.get("/api/projects/{project_id}", response_model=schemas.ProjectDetail)
def get_project(project_id: int, db: Session = Depends(get_db)):
    """Детали проекта со страницами и замечаниями."""
    project = crud.get_project(db, project_id)
    if not project:
        raise HTTPException(404, "Project not found")
    return project


@app.delete("/api/projects/{project_id}")
def delete_project(project_id: int, db: Session = Depends(get_db)):
    """Удалить проект и все связанные данные."""
    project = crud.get_project(db, project_id)
    if not project:
        raise HTTPException(404, "Project not found")
    
    # Удалить файлы проекта
    if project.output_folder:
        import shutil
        try:
            shutil.rmtree(project.output_folder, ignore_errors=True)
        except:
            pass
    
    db.delete(project)
    db.commit()
    return {"status": "deleted", "project_id": project_id}


@app.post("/api/projects/{project_id}/analyze")
def analyze_project(project_id: int, use_tiling: bool = False, db: Session = Depends(get_db)):
    """Запустить анализ (OCR + Layout + QC + DZI) для проекта."""
    project = crud.get_project(db, project_id)
    if not project:
        raise HTTPException(404, "Project not found")
    
    if project.status == "processing":
        return {"status": "already_processing", "project_id": project_id}
    
    if project.status == "completed":
        pass
    
    pdf_files = list(UPLOAD_DIR.glob(f"*_{project.pdf_filename}"))
    if not pdf_files:
        pdf_files = list(UPLOAD_DIR.glob(project.pdf_filename))
    
    if not pdf_files:
        raise HTTPException(404, f"PDF file not found in {UPLOAD_DIR}")
    
    pdf_path = pdf_files[0]
    
    crud.update_project_status(db, project_id, "processing")
    
    import threading
    def run_in_background():
        db_local = next(get_db())
        try:
            processing.run_pipeline(project.id, pdf_path, OUTPUT_DIR, db_local, use_tiling=use_tiling)
        except Exception as e:
            print(f"[ERROR] Pipeline failed for project {project.id}: {e}")
            import traceback
            traceback.print_exc()
            crud.update_project_status(db_local, project.id, "error", error_message=str(e))
        finally:
            db_local.close()
    
    thread = threading.Thread(target=run_in_background)
    thread.start()
    
    return {"status": "processing_started", "project_id": project_id, "ocr_engine": "tiling" if use_tiling else "standard"}


# ==================== ISSUES ====================

@app.get("/api/projects/{project_id}/issues")
def get_issues(
    project_id: int,
    severity: Optional[str] = Query(None, description="error / warning / info"),
    issue_type: Optional[str] = Query(None),
    has_feedback: Optional[bool] = Query(None, description="Filter by feedback presence"),
    db: Session = Depends(get_db)
):
    """Получить замечания проекта."""
    issues = crud.get_issues(
        db, project_id=project_id, severity=severity, 
        issue_type=issue_type, has_feedback=has_feedback
    )
    
    # Build response with page_number manually
    result = []
    for issue in issues:
        item = {
            "id": issue.id,
            "project_id": issue.project_id,
            "page_id": issue.page_id,
            "page_number": issue.page.page_number if issue.page else None,
            "issue_type": issue.issue_type,
            "severity": issue.severity,
            "message": issue.message,
            "bbox_x1": issue.bbox_x1,
            "bbox_y1": issue.bbox_y1,
            "bbox_x2": issue.bbox_x2,
            "bbox_y2": issue.bbox_y2,
            "dimension_text": issue.dimension_text,
            "confidence": issue.confidence,
            "created_at": issue.created_at.isoformat() if issue.created_at else None,
            "feedback": {
                "id": issue.feedback.id,
                "issue_id": issue.feedback.issue_id,
                "is_true_positive": issue.feedback.is_true_positive,
                "comment": issue.feedback.comment,
                "action_taken": issue.feedback.action_taken,
                "created_at": issue.feedback.created_at.isoformat() if issue.feedback.created_at else None
            } if issue.feedback else None
        }
        result.append(item)
    
    return result


@app.get("/api/issues/{issue_id}", response_model=schemas.IssueResponse)
def get_issue(issue_id: int, db: Session = Depends(get_db)):
    """Одно замечание."""
    issue = crud.get_issue(db, issue_id)
    if not issue:
        raise HTTPException(404, "Issue not found")
    return issue


# ==================== FEEDBACK ====================

@app.post("/api/feedback", response_model=schemas.FeedbackResponse)
def submit_feedback(feedback: schemas.FeedbackCreate, db: Session = Depends(get_db)):
    """
    Отправить feedback по замечанию.
    
    is_true_positive:
        - true  = реальная проблема (правильное срабатывание)
        - false = ложное срабатывание (false positive)
        - null  = не уверен
    
    action_taken: fixed / ignored / not_sure
    """
    issue = crud.get_issue(db, feedback.issue_id)
    if not issue:
        raise HTTPException(404, "Issue not found")
    
    return crud.create_feedback(db, feedback)


@app.get("/api/feedback/stats")
def feedback_stats(project_id: Optional[int] = None, db: Session = Depends(get_db)):
    """Статистика feedback."""
    return crud.get_feedback_stats(db, project_id=project_id)


# ==================== VIEWER ====================

@app.get("/viewer/{project_id}/{page_number}", response_class=HTMLResponse)
def get_viewer(project_id: int, page_number: int, db: Session = Depends(get_db)):
    """HTML viewer с overlay замечаний + feedback buttons."""
    project = crud.get_project(db, project_id)
    if not project or not project.output_folder:
        raise HTTPException(404, "Project or output not found")
    
    # ВСЕГДА перегенерировать viewer для нужной страницы
    # (иначе тайлы указывают на другую страницу)
    result = processing.generate_viewer_html(db, project_id, page_number)
    if not result:
        raise HTTPException(404, "Viewer not available")
    
    viewer_path = Path(result)
    content = viewer_path.read_text(encoding="utf-8")
    
    # Подменить пути тайлов — теперь они будут правильные
    # (generate_web_viewer.py уже сгенерировал для page_{page_number:03d}_files/)
    content = content.replace(
        'Url: "./page_', 
        f'Url: "/viewer_tiles/{project_id}/page_'
    )
    
    # Внедрить project_id и page info для навигации
    total_pages = len(project.pages) if project.pages else page_number
    content = content.replace('const PROJECT_ID = null;', f'const PROJECT_ID = {project_id};')
    content = content.replace('const TOTAL_PAGES = null;', f'const TOTAL_PAGES = {total_pages};')
    content = content.replace('const CURRENT_PAGE = null;', f'const CURRENT_PAGE = {page_number};')
    
    # Обновить счётчик страниц в навбаре
    content = content.replace(
        f'<span id="totalPages">?</span>',
        f'<span id="totalPages">{total_pages}</span>'
    )
    
    # Получить issue IDs из БД для этой страницы
    page = crud.get_page_by_number(db, project_id, page_number)
    if page:
        issues = crud.get_issues(db, project_id=project_id, page_id=page.id)
        issue_db_ids = [str(i.id) for i in issues]
        
        # Внедрить DB IDs в HTML
        content = content.replace('data-has-api="false"', 'data-has-api="true"')
        
        def inject_db_id(match):
            idx = int(match.group(1)) - 1
            if idx < len(issue_db_ids):
                return match.group(0) + f' data-db-id="{issue_db_ids[idx]}"'
            return match.group(0)
        
        content = re.sub(r'data-id="(\d+)"', inject_db_id, content)
    
    return HTMLResponse(content=content)


@app.get("/viewer_tiles/{project_id}/{filename:path}")
def get_tile(project_id: int, filename: str, db: Session = Depends(get_db)):
    """Отдаёт DZI тайлы."""
    project = crud.get_project(db, project_id)
    if not project or not project.output_folder:
        raise HTTPException(404)
    
    tile_path = Path(project.output_folder) / filename
    if tile_path.exists() and str(tile_path).startswith(str(project.output_folder)):
        return FileResponse(tile_path)
    raise HTTPException(404)


# ==================== TRAINING DATA ====================

@app.get("/api/training/data", response_model=schemas.TrainingDataExport)
def export_training_data(
    project_id: Optional[int] = Query(None),
    only_labeled: bool = Query(True),
    format: str = Query("json", description="json / yolo / csv"),
    db: Session = Depends(get_db)
):
    """
    Экспорт данных для обучения ML-модели.
    
    only_labeled=true — только размеченные feedback'ом замечания.
    """
    samples = crud.export_training_data(db, project_id=project_id, only_labeled=only_labeled)
    
    if format == "json":
        return schemas.TrainingDataExport(
            total_samples=len(samples),
            labeled_samples=len([s for s in samples if s.is_true_positive is not None]),
            samples=samples,
            export_format="json"
        )
    
    # TODO: YOLO / CSV форматы
    raise HTTPException(400, f"Format {format} not yet implemented")


@app.post("/api/training/export")
def download_training_export(
    project_id: Optional[int] = None,
    only_labeled: bool = True,
    db: Session = Depends(get_db)
):
    """Скачать training data как JSON файл."""
    samples = crud.export_training_data(db, project_id=project_id, only_labeled=only_labeled)
    
    export = {
        "generated_at": datetime.utcnow().isoformat(),
        "total_samples": len(samples),
        "samples": [s.dict() for s in samples]
    }
    
    return JSONResponse(content=export, media_type="application/json")


# ==================== STATS ====================

@app.get("/api/stats", response_model=schemas.StatsResponse)
def get_stats(db: Session = Depends(get_db)):
    """Общая статистика системы."""
    return crud.get_stats(db)


# ==================== HEALTH ====================

@app.get("/api/health")
def health_check():
    return {"status": "ok", "version": "0.1.0"}


# ==================== STATIC (для демо) ====================

# Подключить статику
try:
    app.mount("/", StaticFiles(directory=str(STATIC_DIR), html=True), name="static")
except RuntimeError:
    pass  # Already mounted


if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)
