"""/api/notes — captures (raw field notes) + comments + targets + inference.

Port of v1's capture APIs (main.py:8352-8485), transcribe (8328-8349) and
comment routes (6647-6875), JSON-shaped for the SPA. Routers stay thin: all
behavior lives in services/notes.py; transcription in services/ai/transcribe.

Sync ``def`` throughout EXCEPT the transcribe route (AI call): that one is
``async def`` and — per standards — never holds a DB session across the
await (two short sessions around the Gemini call).

Literal paths (/queue, /inferred-targets, /comments/...) are registered
before the parameterized /{note_id} routes.
"""
from __future__ import annotations

from datetime import date, datetime

from fastapi import APIRouter, Depends, File, Form, Request, UploadFile
from sqlalchemy.orm import Session

from ..db import get_db, get_engine
from ..deps import current_garden_id, current_user
from ..errors import AppError, VALIDATION_FAILED
from ..schemas.notes import (
    CaptureListOut,
    CaptureOut,
    CaptureQueueOut,
    CaptureUpdate,
    CommentOut,
    CommentTargetsOut,
    InferredTargetsOut,
    OkOut,
    TargetRef,
    TranscriptOut,
)
from ..services import notes as notes_svc
from ..services.ai.transcribe import transcribe_audio

router = APIRouter(prefix="/api/notes", tags=["notes"])


def _parse_date(value: str, field: str) -> date:
    try:
        return date.fromisoformat((value or "").strip())
    except ValueError:
        raise AppError(VALIDATION_FAILED, f"{field} must be YYYY-MM-DD", 400)


def _parse_datetime(value: str, field: str) -> datetime | None:
    value = (value or "").strip()
    if not value:
        return None
    try:
        return datetime.fromisoformat(value.replace("Z", "+00:00"))
    except ValueError:
        raise AppError(VALIDATION_FAILED, f"{field} must be ISO 8601", 400)


def _parse_secondaries(raw: list[str]) -> list[tuple[str, int]]:
    """v1 comment_create: "type:id" strings; malformed entries skipped."""
    out: list[tuple[str, int]] = []
    for s in raw or []:
        if ":" not in s:
            continue
        t_str, i_str = s.split(":", 1)
        if t_str not in notes_svc.COMMENT_TARGET_TYPES:
            continue
        try:
            out.append((t_str, int(i_str)))
        except ValueError:
            continue
    return out


# --- capture queue (literal paths first) -----------------------------------

@router.get("/queue", response_model=CaptureQueueOut)
def capture_queue(
    status: str = "new",
    kind: str = "",
    garden_id: int = Depends(current_garden_id),
    db: Session = Depends(get_db),
) -> dict:
    if status not in notes_svc.NOTE_STATUSES and status != "all":
        status = "new"
    return {
        "notes": notes_svc.list_captures(
            db, garden_id, status=status, limit=200, kind=kind
        ),
        "status": status,
        "counts": notes_svc.queue_counts(db, garden_id),
    }


@router.get("/inferred-targets", response_model=InferredTargetsOut)
def inferred_targets(
    type: str,
    id: int,
    date: str = "",
    body: str = "",
    garden_id: int = Depends(current_garden_id),
    db: Session = Depends(get_db),
) -> dict:
    """Preview the NARROWED inferred-secondaries set for a would-be comment.
    ``body`` is optional: mention-gated species/variety/plant links only
    appear once the drafted body text names them."""
    if type not in notes_svc.COMMENT_TARGET_TYPES:
        raise AppError(notes_svc.NOTE_INVALID_TARGET_TYPE, "invalid target type", 400)
    comment_date = _parse_date(date, "date") if date.strip() else None
    pairs = notes_svc.inferred_targets_for(
        db, garden_id, type, id, comment_date, body_text=body
    )
    by_id = notes_svc.load_area_maps(db, garden_id)
    return {
        "targets": [
            notes_svc._target_out(db, t, i, by_id) for t, i in pairs
        ]
    }


# --- comments ---------------------------------------------------------------

@router.post("/comments", response_model=CommentOut, status_code=201)
def comment_create(
    target_type: str = Form(...),
    target_id: int = Form(...),
    comment_date: str = Form(...),
    body: str = Form(""),
    kind: str = Form(""),
    transcript: str = Form(""),
    captured_at: str = Form(""),
    audio: UploadFile | None = File(None),
    photos: list[UploadFile] = File(default=[]),
    secondary_target: list[str] = Form(default=[]),
    override_inference: str = Form(""),
    page_context: str = Form(""),
    garden_id: int = Depends(current_garden_id),
    db: Session = Depends(get_db),
) -> dict:
    explicit = (
        _parse_secondaries(secondary_target) if override_inference == "1" else None
    )
    return notes_svc.create_comment(
        db,
        garden_id,
        target_type=target_type,
        target_id=target_id,
        comment_date=_parse_date(comment_date, "comment_date"),
        body=body,
        kind=kind,
        transcript=transcript,
        captured_at=_parse_datetime(captured_at, "captured_at"),
        audio=audio,
        photos=photos,
        explicit_secondaries=explicit,
        page_context=page_context,
    )


@router.patch("/comments/{comment_id}", response_model=CommentOut)
def comment_update(
    comment_id: int,
    comment_date: str | None = Form(None),
    body: str | None = Form(None),
    kind: str | None = Form(None),
    remove_audio: str = Form(""),
    remove_photos: list[str] = Form(default=[]),
    audio: UploadFile | None = File(None),
    photos: list[UploadFile] = File(default=[]),
    garden_id: int = Depends(current_garden_id),
    db: Session = Depends(get_db),
) -> dict:
    return notes_svc.update_comment(
        db,
        garden_id,
        comment_id,
        comment_date=(
            _parse_date(comment_date, "comment_date")
            if comment_date is not None
            else None
        ),
        body=body,
        kind=kind,
        audio=audio,
        remove_audio=bool(remove_audio.strip()),  # v1: any truthy value
        photos=photos,
        remove_photos=remove_photos,
    )


@router.delete("/comments/{comment_id}", response_model=OkOut)
def comment_delete(
    comment_id: int,
    garden_id: int = Depends(current_garden_id),
    db: Session = Depends(get_db),
) -> dict:
    notes_svc.delete_field_note(db, garden_id, comment_id)
    return {"ok": True}


@router.get("/comments/{comment_id}/targets", response_model=CommentTargetsOut)
def comment_targets(
    comment_id: int,
    garden_id: int = Depends(current_garden_id),
    db: Session = Depends(get_db),
) -> dict:
    return {"targets": notes_svc.load_comment_targets(db, garden_id, comment_id)}


@router.post("/comments/{comment_id}/targets", response_model=CommentTargetsOut)
def comment_target_add(
    comment_id: int,
    payload: TargetRef,
    garden_id: int = Depends(current_garden_id),
    db: Session = Depends(get_db),
) -> dict:
    return {
        "targets": notes_svc.add_comment_target(
            db, garden_id, comment_id, payload.type, payload.id
        )
    }


@router.delete(
    "/comments/{comment_id}/targets/{target_type}/{target_id}",
    response_model=CommentTargetsOut,
)
def comment_target_remove(
    comment_id: int,
    target_type: str,
    target_id: int,
    garden_id: int = Depends(current_garden_id),
    db: Session = Depends(get_db),
) -> dict:
    return {
        "targets": notes_svc.remove_comment_target(
            db, garden_id, comment_id, target_type, target_id
        )
    }


# --- captures ----------------------------------------------------------------

@router.post("", response_model=CaptureOut, status_code=201)
def capture_create(
    audio: UploadFile | None = File(None),
    photos: list[UploadFile] = File(default=[]),
    text: str = Form(""),
    transcript: str = Form(""),
    captured_at: str = Form(""),
    kind: str = Form(""),
    page_context: str = Form(""),
    garden_id: int = Depends(current_garden_id),
    db: Session = Depends(get_db),
) -> dict:
    return notes_svc.create_capture(
        db,
        garden_id,
        text=text,
        transcript=transcript,
        captured_at=_parse_datetime(captured_at, "captured_at"),
        audio=audio,
        photos=photos,
        kind=kind,
        page_context=page_context,
    )


@router.get("", response_model=CaptureListOut)
def capture_list(
    status: str = "new",
    limit: int = 50,
    kind: str = "",
    garden_id: int = Depends(current_garden_id),
    db: Session = Depends(get_db),
) -> dict:
    return {
        "notes": notes_svc.list_captures(
            db, garden_id, status=status, limit=limit, kind=kind
        )
    }


@router.patch("/{note_id}", response_model=CaptureOut)
def capture_update(
    note_id: int,
    payload: CaptureUpdate,
    garden_id: int = Depends(current_garden_id),
    db: Session = Depends(get_db),
) -> dict:
    return notes_svc.update_capture(
        db, garden_id, note_id, payload.model_dump(exclude_unset=True)
    )


@router.delete("/{note_id}", response_model=OkOut)
def capture_delete(
    note_id: int,
    garden_id: int = Depends(current_garden_id),
    db: Session = Depends(get_db),
) -> dict:
    notes_svc.delete_field_note(db, garden_id, note_id)
    return {"ok": True}


@router.post("/{note_id}/transcribe", response_model=TranscriptOut)
async def capture_transcribe(note_id: int, request: Request) -> dict:
    """AI route (async). Per standards, no DB session is held across the
    await: session 1 resolves the caller + audio path, then closes; the
    Gemini call runs; session 2 stores the transcript."""
    with Session(get_engine()) as db:
        user = current_user(request, db)
        garden_id = current_garden_id(user, db)
        abs_path = notes_svc.get_capture_audio_abs_path(db, garden_id, note_id)
        db.commit()  # persist auto-provisioned user/membership, if any
    transcript = await transcribe_audio(abs_path)
    with Session(get_engine()) as db:
        notes_svc.set_capture_transcript(db, garden_id, note_id, transcript)
    return {"ok": True, "id": note_id, "transcript": transcript}
