"""
FastAPI verification service for Verisoo.

Lightweight REST API for:
- Verifying images against attestation records
- Looking up attestations by ID
- Getting proof links

Designed for media orgs and fact-checkers to integrate easily.

Run with: uvicorn verisoo.api:app --host 0.0.0.0 --port 8000
"""

from __future__ import annotations

import os
from datetime import datetime
from pathlib import Path
from typing import Annotated

try:
    from fastapi import FastAPI, File, Form, HTTPException, Query, UploadFile
    from fastapi.middleware.cors import CORSMiddleware
    from fastapi.responses import JSONResponse
    from pydantic import BaseModel
except ImportError:
    raise ImportError("API requires fastapi: pip install verisoo[api]")

from .hashing import compute_all_distances, hash_image, is_same_image
from .models import AttestationRecord, ImageHashes, ProofLink
from .storage import LocalStorage
from .crypto import verify_signature, load_public_key_from_bytes

# Configuration via environment
DATA_DIR = Path(os.environ.get("SOOSEF_DATA_DIR", Path.home() / ".soosef"))
BASE_URL = os.environ.get("VERISOO_BASE_URL", "https://verisoo.io")

app = FastAPI(
    title="Verisoo",
    description="Decentralized image provenance and attestation API",
    version="0.1.0",
    docs_url="/docs",
    redoc_url="/redoc",
)

# CORS for browser-based verification widgets
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # Configure appropriately for production
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


# --- Response models ---


class LocationResponse(BaseModel):
    latitude: float
    longitude: float
    accuracy_meters: float | None = None
    name: str | None = None


class DeviceResponse(BaseModel):
    make: str | None = None
    model: str | None = None
    software: str | None = None


class AttestorResponse(BaseModel):
    fingerprint: str
    name: str | None = None


class AttestationResponse(BaseModel):
    record_id: str
    short_id: str
    attestor: AttestorResponse
    attested_at: datetime
    captured_at: datetime | None = None
    location: LocationResponse | None = None
    device: DeviceResponse | None = None
    caption: str | None = None
    proof_url: str
    match_type: str  # "exact", "phash", "dhash", etc.
    hash_distances: dict[str, int]


class VerifyResponse(BaseModel):
    verified: bool
    message: str
    attestations: list[AttestationResponse]
    image_sha256: str
    checked_at: datetime


class StatusResponse(BaseModel):
    status: str
    record_count: int
    merkle_root: str | None
    log_size_bytes: int


class ProofResponse(BaseModel):
    record_id: str
    short_id: str
    attestor: AttestorResponse
    attested_at: datetime
    captured_at: datetime | None = None
    location: LocationResponse | None = None
    device: DeviceResponse | None = None
    caption: str | None = None
    image_sha256: str
    proof_url: str
    full_proof_url: str


# --- Helper functions ---


def get_storage() -> LocalStorage:
    """Get storage instance (could be cached/pooled in production)."""
    return LocalStorage(DATA_DIR)


def record_to_attestation_response(
    record,
    identity,
    match_type: str,
    distances: dict[str, int],
) -> AttestationResponse:
    """Convert internal record to API response."""
    cm = record.capture_metadata
    proof = ProofLink(record.record_id, BASE_URL)

    location = None
    if record.location:
        loc = record.location
        location = LocationResponse(
            latitude=loc.latitude,
            longitude=loc.longitude,
            accuracy_meters=loc.accuracy_meters,
            name=loc.location_name,
        )

    device = None
    if cm and cm.device:
        dev = cm.device
        device = DeviceResponse(
            make=dev.make,
            model=dev.model,
            software=dev.software,
        )

    return AttestationResponse(
        record_id=record.record_id,
        short_id=record.short_id,
        attestor=AttestorResponse(
            fingerprint=record.attestor_fingerprint,
            name=identity.display_name if identity else None,
        ),
        attested_at=record.timestamp,
        captured_at=record.captured_at,
        location=location,
        device=device,
        caption=cm.caption if cm else None,
        proof_url=proof.url,
        match_type=match_type,
        hash_distances=distances,
    )


# --- Endpoints ---


@app.get("/", response_class=JSONResponse)
async def root():
    """API root - basic info."""
    return {
        "service": "Verisoo",
        "description": "Decentralized image provenance and attestation",
        "docs": "/docs",
        "verify": "POST /verify with image file",
    }


@app.get("/status", response_model=StatusResponse)
async def status():
    """Get service status and statistics."""
    storage = get_storage()
    stats = storage.get_stats()

    return StatusResponse(
        status="ok",
        record_count=stats.record_count,
        merkle_root=stats.merkle_root,
        log_size_bytes=stats.log_file_size,
    )


@app.post("/verify", response_model=VerifyResponse)
async def verify_image(
    image: Annotated[UploadFile, File(description="Image to verify")],
    threshold: Annotated[int, Query(ge=0, le=50)] = 10,
):
    """
    Verify an image against known attestations.

    Upload an image (even a mangled social media version) and get back
    any matching attestations with provenance information.

    The threshold parameter controls how strict matching is:
    - 0: Exact perceptual match only
    - 5: Very strict
    - 10: Default - catches most social media compression
    - 15-20: Looser - may catch more heavily modified images
    """
    # Read and hash the uploaded image
    image_data = await image.read()

    try:
        image_hashes = hash_image(image_data, robust=True)
    except Exception as e:
        raise HTTPException(status_code=400, detail=f"Invalid image: {e}")

    storage = get_storage()
    matches: list[AttestationResponse] = []

    # Search for matching attestations
    for record in storage.iterate_records():
        is_match, match_type = is_same_image(
            image_hashes,
            record.image_hashes,
            perceptual_threshold=threshold,
        )

        if is_match:
            # Get attestor identity for display name
            identity = storage.load_identity(record.attestor_fingerprint)

            # Compute distances for transparency
            distances = compute_all_distances(image_hashes, record.image_hashes)

            matches.append(
                record_to_attestation_response(record, identity, match_type, distances)
            )

    # Sort by attestation time (oldest first - establishes priority)
    matches.sort(key=lambda m: m.attested_at)

    if matches:
        first = matches[0]
        if first.location:
            message = f"Image attested by {first.attestor.name or first.attestor.fingerprint[:8]} in {first.location.name or 'recorded location'} on {first.attested_at.strftime('%Y-%m-%d')}"
        else:
            message = f"Image attested by {first.attestor.name or first.attestor.fingerprint[:8]} on {first.attested_at.strftime('%Y-%m-%d')}"
    else:
        message = "No matching attestations found"

    return VerifyResponse(
        verified=len(matches) > 0,
        message=message,
        attestations=matches,
        image_sha256=image_hashes.sha256,
        checked_at=datetime.utcnow(),
    )


@app.get("/v/{short_id}", response_model=ProofResponse)
async def get_proof_short(short_id: str):
    """
    Get attestation proof by short ID.

    This is the endpoint for shareable proof links:
    verisoo.io/v/a8f3c2d1e9b7
    """
    return await _get_proof(short_id)


@app.get("/verify/{record_id}", response_model=ProofResponse)
async def get_proof_full(record_id: str):
    """Get attestation proof by full record ID."""
    return await _get_proof(record_id)


async def _get_proof(id_prefix: str) -> ProofResponse:
    """Internal: look up proof by ID prefix."""
    storage = get_storage()

    # Search for matching record
    for record in storage.iterate_records():
        if record.record_id.startswith(id_prefix) or record.short_id == id_prefix:
            identity = storage.load_identity(record.attestor_fingerprint)
            cm = record.capture_metadata
            proof = ProofLink(record.record_id, BASE_URL)

            location = None
            if record.location:
                loc = record.location
                location = LocationResponse(
                    latitude=loc.latitude,
                    longitude=loc.longitude,
                    accuracy_meters=loc.accuracy_meters,
                    name=loc.location_name,
                )

            device = None
            if cm and cm.device:
                dev = cm.device
                device = DeviceResponse(
                    make=dev.make,
                    model=dev.model,
                    software=dev.software,
                )

            return ProofResponse(
                record_id=record.record_id,
                short_id=record.short_id,
                attestor=AttestorResponse(
                    fingerprint=record.attestor_fingerprint,
                    name=identity.display_name if identity else None,
                ),
                attested_at=record.timestamp,
                captured_at=record.captured_at,
                location=location,
                device=device,
                caption=cm.caption if cm else None,
                image_sha256=record.image_hashes.sha256,
                proof_url=proof.url,
                full_proof_url=proof.full_url,
            )

    raise HTTPException(status_code=404, detail=f"Attestation not found: {id_prefix}")


@app.get("/attestor/{fingerprint}")
async def get_attestor_records(
    fingerprint: str,
    limit: Annotated[int, Query(ge=1, le=100)] = 20,
):
    """Get recent attestations by an attestor."""
    storage = get_storage()

    # Check if this is a known identity
    identity = storage.load_identity(fingerprint)

    records = storage.get_records_by_attestor(fingerprint)
    recent = records[-limit:] if len(records) > limit else records

    return {
        "attestor": {
            "fingerprint": fingerprint,
            "name": identity.display_name if identity else None,
            "known": identity is not None,
        },
        "total_attestations": len(records),
        "recent": [
            {
                "record_id": r.record_id,
                "short_id": r.short_id,
                "attested_at": r.timestamp.isoformat(),
                "image_sha256": r.image_hashes.sha256[:16] + "...",
                "proof_url": ProofLink(r.record_id, BASE_URL).url,
            }
            for r in recent
        ],
    }


# --- Mobile sync endpoint ---


class MobileAttestationRequest(BaseModel):
    """Attestation submitted from mobile app."""
    sha256: str  # Image SHA-256 (mobile computed)
    attestor_fingerprint: str
    attestor_public_key: str  # Hex-encoded Ed25519 public key
    timestamp: datetime
    signature: str  # Hex-encoded Ed25519 signature
    metadata: dict | None = None  # Location, caption, device, etc.


class MobileAttestationResponse(BaseModel):
    """Response after processing mobile attestation."""
    success: bool
    record_id: str
    short_id: str
    proof_url: str
    embedded_image_available: bool
    message: str


@app.post("/attest", response_model=MobileAttestationResponse)
async def attest_from_mobile(
    image: Annotated[UploadFile, File(description="Original image from mobile")],
    attestation: Annotated[str, Form(description="JSON attestation data")],
):
    """
    Receive attestation from mobile app and finalize it.

    Mobile sends:
    - Original image bytes
    - Pre-signed attestation (SHA-256 + signature + metadata)

    Server:
    1. Verifies signature
    2. Computes perceptual hashes (pHash, dHash)
    3. Embeds proof link via DCT (JPEG only)
    4. Stores in log
    5. Returns confirmation + embedded image endpoint

    The mobile app can then download the embedded image.
    """
    import base64
    import json

    # Parse attestation JSON
    try:
        data = json.loads(attestation)
        req = MobileAttestationRequest(**data)
    except Exception as e:
        raise HTTPException(status_code=400, detail=f"Invalid attestation data: {e}")

    # Read image
    image_data = await image.read()

    # Verify SHA-256 matches
    from .hashing import hash_image
    computed_hashes = hash_image(image_data, robust=True)

    if computed_hashes.sha256 != req.sha256:
        raise HTTPException(
            status_code=400,
            detail=f"SHA-256 mismatch: expected {req.sha256[:16]}..., got {computed_hashes.sha256[:16]}..."
        )

    # Verify signature
    try:
        public_key_bytes = bytes.fromhex(req.attestor_public_key)
        signature_bytes = bytes.fromhex(req.signature)

        # Reconstruct signing payload (must match mobile's format)
        signing_payload = f"{req.sha256}|{req.attestor_fingerprint}|{req.timestamp.isoformat()}"
        if req.metadata:
            signing_payload += f"|{json.dumps(req.metadata, sort_keys=True, separators=(',', ':'))}"

        is_valid = verify_signature(
            signing_payload.encode(),
            signature_bytes,
            public_key_bytes,
        )

        if not is_valid:
            raise HTTPException(status_code=401, detail="Invalid signature")

    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=400, detail=f"Signature verification failed: {e}")

    # Create full attestation record with perceptual hashes
    record = AttestationRecord(
        image_hashes=computed_hashes,  # Includes pHash, dHash from server
        attestor_fingerprint=req.attestor_fingerprint,
        timestamp=req.timestamp,
        signature=signature_bytes,
        metadata=req.metadata or {},
    )

    # Store in log
    storage = get_storage()
    index = storage.append_record(record)

    # Update Merkle tree
    merkle_log = storage.load_merkle_log()
    storage.set_merkle_root(merkle_log.root_hash)

    # Try to embed proof link (JPEG only)
    embedded_available = False
    proof_link = f"{BASE_URL}/v/{record.short_id}"

    try:
        from .embed import embed_proof_link, get_embed_method
        from pathlib import Path
        import tempfile

        # Check if we can embed (JPEG with stegasoo available)
        # Save image temporarily to check format
        with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as f:
            f.write(image_data)
            temp_path = Path(f.name)

        method = get_embed_method(temp_path)

        if method == "dct":
            from .embed import embed_proof_in_jpeg

            embedded_bytes, stats = embed_proof_in_jpeg(image_data, proof_link)

            # Save embedded image for later retrieval
            embedded_path = storage.base_path / "embedded" / f"{record.short_id}.jpg"
            embedded_path.parent.mkdir(parents=True, exist_ok=True)
            embedded_path.write_bytes(embedded_bytes)
            embedded_available = True

        # Cleanup temp file
        temp_path.unlink(missing_ok=True)

    except Exception as e:
        # DCT embedding failed, continue without it
        import logging
        logging.warning(f"DCT embedding failed: {e}")

    # Register the attestor's identity if not known
    from .models import Identity
    if not storage.load_identity(req.attestor_fingerprint):
        identity = Identity(
            public_key=public_key_bytes,
            fingerprint=req.attestor_fingerprint,
        )
        storage.save_identity(identity)

    return MobileAttestationResponse(
        success=True,
        record_id=record.record_id,
        short_id=record.short_id,
        proof_url=proof_link,
        embedded_image_available=embedded_available,
        message=f"Attestation stored at index {index}" + (
            ", embedded image available" if embedded_available else ""
        ),
    )


@app.get("/embedded/{short_id}")
async def get_embedded_image(short_id: str):
    """
    Download the DCT-embedded version of an attested image.

    Only available for JPEG images that were successfully embedded.
    """
    from fastapi.responses import FileResponse

    storage = get_storage()
    embedded_path = storage.base_path / "embedded" / f"{short_id}.jpg"

    if not embedded_path.exists():
        raise HTTPException(
            status_code=404,
            detail="Embedded image not available. Either not a JPEG or embedding failed."
        )

    return FileResponse(
        embedded_path,
        media_type="image/jpeg",
        filename=f"{short_id}_attested.jpg",
    )


# --- Health check ---


@app.get("/health")
async def health():
    """Health check for load balancers."""
    return {"status": "healthy"}


# --- Run directly ---


def serve(host: str = "0.0.0.0", port: int = 8000):
    """Run the API server."""
    import uvicorn

    uvicorn.run(app, host=host, port=port)


if __name__ == "__main__":
    serve()
