from __future__ import annotations

from fastapi import APIRouter, HTTPException, Request, Depends
from pydantic import BaseModel
from typing import Optional
import logging
import base64
import json
import os
from datetime import datetime, timedelta
from uuid import UUID, uuid4
from pathlib import PurePosixPath

from ...storage import get_storage_backend, resolve_object_path
from ... import settings
from ...auth import get_current_account
import hmac
import hashlib


log = logging.getLogger(__name__)

router = APIRouter(prefix="/api/v2/uploads", tags=["uploads-v2"])


class PresignReq(BaseModel):
    content_type: str = "text/csv"


class PresignResp(BaseModel):
    object_id: str
    put_url: str
    get_url: str


def _get_secret() -> str:
    # Prefer explicit SECRET_KEY if present; fall back to webhook or JWT secrets; last resort dev default
    secret = (
        getattr(settings, "SECRET_KEY", None)
        or getattr(settings, "WEBHOOK_HMAC_SECRET", None)
        or getattr(settings, "JWT_SECRET_KEY", None)
    )
    return secret or "dev-secret"


def _make_token(payload: dict, secret: str) -> str:
    body = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode()
    sig = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    return base64.urlsafe_b64encode(body).decode() + "." + sig


def _build_object_id(ext: str) -> str:
    prefix = os.getenv("FM_UPLOAD_PREFIX", "uploads")
    cleaned = "/".join(part for part in str(prefix).strip("/").split("/") if part)
    base = PurePosixPath(cleaned or "uploads")
    # PurePosixPath ensures forward slashes regardless of OS
    return str(base / f"{uuid4()}{ext}")


def _maybe_make_absolute(url: str) -> str:
    base = os.getenv("PUBLIC_BASE_URL") or getattr(settings, "PUBLIC_BASE_URL", "")
    if not url or url.startswith("http://") or url.startswith("https://"):
        return url
    if not base:
        return url
    base = str(base).rstrip("/")
    normalized = url if str(url).startswith("/") else f"/{url}"
    return f"{base}{normalized}"


def _verify_token(token: str, secret: str) -> dict:
    try:
        body_b64, sig = token.split(".", 1)
    except ValueError:
        raise HTTPException(401, "invalid token format")
    body = base64.urlsafe_b64decode(body_b64.encode())
    exp_sig = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(sig, exp_sig):
        raise HTTPException(401, "bad signature")
    try:
        payload = json.loads(body)
    except Exception:
        raise HTTPException(401, "invalid token body")
    if int(payload.get("exp", 0)) < int(datetime.utcnow().timestamp()):
        raise HTTPException(401, "token expired")
    return payload


from fastapi import Response


@router.post("/presign", response_model=PresignResp)
async def presign_upload(
    req: PresignReq,
    response: Response,
    account_id: UUID = Depends(get_current_account),
) -> PresignResp:
    """Create an object_id and return presigned PUT/GET URLs.

    - For S3: returns real presigned PUT/GET URLs
    - For local: returns a tokenized PUT endpoint handled below and a local download URL
    """
    storage = get_storage_backend()

    # Create storage key (object id)
    ext = ".csv" if not req.content_type or "csv" in req.content_type else ""
    object_id = _build_object_id(ext)
    log.info(
        "presign_upload: object_id=%s content_type=%s", object_id, req.content_type
    )

    # S3-like backends can generate real presigned URLs; local uses a special PUT receiver
    if settings.STORAGE_BACKEND != "local":
        try:
            put_url = await storage.generate_presigned_url(
                object_id, operation="put_object"
            )
            get_url = await storage.generate_presigned_url(
                object_id, operation="get_object"
            )
            response.headers["Cache-Control"] = "no-store"
            return PresignResp(object_id=object_id, put_url=put_url, get_url=get_url)
        except Exception as e:
            raise HTTPException(500, f"presign_failed: {e}")

    # Local dev: build token and URLs
    ttl = int(os.getenv("SIGNED_URL_TTL_SEC", "86400"))
    token = _make_token(
        {
            "key": object_id,
            "ct": req.content_type,
            "acct": str(account_id),
            "exp": int((datetime.utcnow() + timedelta(seconds=ttl)).timestamp()),
        },
        _get_secret(),
    )
    put_url = _maybe_make_absolute(f"/api/v2/uploads/put/{object_id}?token={token}")

    # Reuse storage helper for GET URL (local implementation returns a download endpoint)
    try:
        get_url = await storage.generate_presigned_url(
            object_id, operation="get_object"
        )
    except Exception:
        # Last resort: expose same local download handler path
        get_url = f"/api/v1/storage/download/{object_id}?token={token}"

    get_url = _maybe_make_absolute(get_url)
    response.headers["Cache-Control"] = "no-store"
    return PresignResp(object_id=object_id, put_url=put_url, get_url=get_url)


@router.put("/put/{object_id:path}")
async def put_object(
    object_id: str,
    request: Request,
    token: Optional[str] = None,
):
    """Local-only PUT receiver used when presign falls back.

    Validates token then writes raw request body bytes to LOCAL_STORAGE_PATH/object_id.
    """
    # Validate token
    if not token:
        raise HTTPException(401, "missing token")
    payload: dict
    account_claim: Optional[str]
    try:
        payload = _verify_token(token, _get_secret())
        if payload.get("key") != object_id:
            raise HTTPException(401, "invalid token key")
        account_claim = payload.get("acct")
        if not account_claim:
            raise HTTPException(401, "token missing account")
        account_claim = str(account_claim)
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(401, f"invalid token: {e}")

    # Only support local backend for this handler
    if settings.STORAGE_BACKEND != "local":
        raise HTTPException(400, "PUT receiver only available for local storage")

    # Optional: enforce content-type loosely based on token
    try:
        ct_claim = payload.get("ct")
        hdr_ct = request.headers.get("content-type")
        if (
            ct_claim
            and hdr_ct
            and not hdr_ct.lower().startswith(ct_claim.split(";")[0].lower().strip())
        ):
            # Allow text/csv vs text/csv; charset=utf-8 variations
            if not (ct_claim.startswith("text/") and hdr_ct.startswith("text/")):
                raise HTTPException(415, "content-type mismatch")
    except HTTPException:
        raise
    except Exception:
        # Be tolerant if headers are missing or odd
        pass

    # Enforce base path and block traversal attempts
    from pathlib import Path

    # Use unified path resolution from storage module
    from ...storage import resolve_uploads_base

    # Get the absolute path using the unified resolver
    try:
        target = Path(resolve_object_path(object_id))
    except ValueError as e:
        raise HTTPException(400, f"invalid object_id: {e}")

    # Log the resolved path
    base_path = resolve_uploads_base()
    log.info(f"[UPLOADS] Writing {object_id} (acct={account_claim}) -> {target}")
    log.info(f"[UPLOADS] Base path: {base_path}")

    # Verify path is within base (redundant but safe)
    if not str(target).startswith(str(base_path)):
        raise HTTPException(400, "invalid object_id path traversal")

    # Stream to disk and enforce a size limit (default 250MB)
    max_bytes = int(os.getenv("FM_MAX_PUT_BYTES", str(250 * 1024 * 1024)))
    total = 0
    target.parent.mkdir(parents=True, exist_ok=True)
    try:
        with open(target, "wb") as f:
            async for chunk in request.stream():
                if not chunk:
                    continue
                total += len(chunk)
                if total > max_bytes:
                    try:
                        target.unlink(missing_ok=True)
                    except Exception:
                        pass
                    raise HTTPException(413, "upload too large")
                f.write(chunk)
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(500, f"write_failed: {e}")

    return {"ok": True, "object_id": object_id, "bytes": total}
