"""Shared dependencies: auth, permissions, utilities for the API."""
from __future__ import annotations

from datetime import datetime, timedelta, timezone
import hashlib
import os
import secrets
from typing import Any
from uuid import uuid4

from fastapi import HTTPException, Request

from api.models import OpenApiExecuteRequest
from api.state import (
    ROLE_LEVEL,
    ROLE_PERMISSIONS,
    STATE_LOCK,
    TOKENS,
    USERS,
)
from src.tools import (
    is_langfuse_openapi_mutating,
    parse_langfuse_control_message,
    run_langfuse_control_message,
    run_langfuse_control_operation,
)
from src.user_store import (
    get_user_langfuse_settings,
    get_user_langfuse_project,
    insert_audit_log,
    list_user_langfuse_projects,
    validate_session_token,
)


# ---------------------------------------------------------------------------
# Utility helpers
# ---------------------------------------------------------------------------

def _now_iso() -> str:
    return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")


def _new_id(prefix: str) -> str:
    return f"{prefix}_{uuid4().hex[:12]}"


def _resolve_thread_id(raw: str | None, *, prefix: str = "thread") -> str:
    text = str(raw or "").strip()
    if text:
        return text[:160]
    return _new_id(prefix)


def _parse_datetime(raw: str | None) -> datetime | None:
    if not raw:
        return None
    text = str(raw).strip()
    if not text:
        return None
    try:
        if len(text) == 10:
            dt = datetime.fromisoformat(f"{text}T00:00:00+00:00")
        else:
            dt = datetime.fromisoformat(text.replace("Z", "+00:00"))
        if dt.tzinfo is None:
            dt = dt.replace(tzinfo=timezone.utc)
        return dt.astimezone(timezone.utc)
    except Exception:
        return None


def _normalize_target(raw: str) -> str:
    target = str(raw or "").strip().lower()
    if target not in {"project", "home"}:
        raise HTTPException(status_code=400, detail="target must be 'project' or 'home'")
    return target


def _env_int(name: str, default: int, minimum: int, maximum: int) -> int:
    raw = str(os.getenv(name, "")).strip()
    if not raw:
        return default
    try:
        value = int(raw)
    except Exception:
        return default
    return max(minimum, min(value, maximum))


def _as_bool(raw: str | bool | None, *, default: bool = False) -> bool:
    if isinstance(raw, bool):
        return raw
    if raw is None:
        return default
    text = str(raw).strip().lower()
    if text in {"1", "true", "yes", "y", "on"}:
        return True
    if text in {"0", "false", "no", "n", "off"}:
        return False
    return default


def _paginate(items: list[dict[str, Any]], page: int, size: int) -> dict[str, Any]:
    safe_page = max(1, int(page or 1))
    safe_size = max(1, min(int(size or 20), 100))
    total = len(items)
    start = (safe_page - 1) * safe_size
    end = start + safe_size
    return {
        "page": safe_page,
        "size": safe_size,
        "total": total,
        "items": items[start:end],
    }


def _mask_secret(value: str) -> str:
    text = str(value or "").strip()
    if not text:
        return ""
    if len(text) <= 6:
        return "*" * len(text)
    return f"{text[:3]}***{text[-2:]}"


def _page_context(active_page: str) -> dict[str, Any]:
    return {"active_page": active_page}


def _extract_metrics(markdown_text: str) -> dict[str, Any]:
    metrics: dict[str, Any] = {}
    in_metrics = False
    for raw_line in (markdown_text or "").splitlines():
        line = raw_line.strip()
        if line.lower().startswith("## metrics summary"):
            in_metrics = True
            continue
        if in_metrics and line.startswith("## "):
            break
        if not in_metrics or not line.startswith("- "):
            continue
        body = line[2:]
        if ":" not in body:
            continue
        key, value = body.split(":", 1)
        key = key.strip()
        value = value.strip()
        if key:
            metrics[key] = value
    return metrics


# ---------------------------------------------------------------------------
# Token / Auth helpers
# ---------------------------------------------------------------------------

def _find_token_record(raw_token: str) -> dict[str, Any] | None:
    if not raw_token:
        return None
    token_hash = hashlib.sha256(raw_token.encode("utf-8")).hexdigest()
    with STATE_LOCK:
        for record in TOKENS.values():
            if record.get("token_hash") == token_hash:
                return dict(record)
    return None


def _extract_bearer_token(request: Request) -> str:
    auth_header = str(request.headers.get("authorization", "")).strip()
    if not auth_header:
        return ""
    if not auth_header.lower().startswith("bearer "):
        raise HTTPException(status_code=401, detail="invalid authorization header")
    token = auth_header.split(" ", 1)[1].strip()
    if not token:
        raise HTTPException(status_code=401, detail="empty bearer token")
    return token


def _auth_allow_signup() -> bool:
    raw = str(os.getenv("AGENT_AUTH_ALLOW_SIGNUP", "true")).strip().lower()
    return raw in {"1", "true", "yes", "y", "on"}


def _bootstrap_admin_token() -> str:
    return str(os.getenv("AGENT_BOOTSTRAP_ADMIN_TOKEN", "")).strip()


def _bootstrap_admin_subject() -> str:
    return str(os.getenv("AGENT_BOOTSTRAP_ADMIN_SUBJECT", "")).strip() or "bootstrap-admin"


def _cleanup_expired_tokens() -> int:
    now = datetime.now(timezone.utc)
    cleaned = 0
    with STATE_LOCK:
        for token in TOKENS.values():
            if token.get("revoked"):
                continue
            expires_at = _parse_datetime(token.get("expires_at"))
            if expires_at and now > expires_at:
                token["revoked"] = True
                token["revoked_at"] = _now_iso()
                token["revoke_reason"] = "expired"
                cleaned += 1
    return cleaned


def _validate_token(raw_token: str) -> dict[str, Any]:
    _cleanup_expired_tokens()
    record = _find_token_record(raw_token)
    if not record:
        raise HTTPException(status_code=401, detail="invalid token")
    if record.get("revoked"):
        raise HTTPException(status_code=401, detail="token revoked")
    expires_at = _parse_datetime(record.get("expires_at"))
    if expires_at and datetime.now(timezone.utc) > expires_at:
        raise HTTPException(status_code=401, detail="token expired")
    with STATE_LOCK:
        token = TOKENS.get(record["token_id"])
        if token is not None:
            token["last_used_at"] = _now_iso()
    return record


def _effective_identity(request: Request) -> dict[str, Any]:
    cached = getattr(request.state, "identity", None)
    if isinstance(cached, dict):
        return cached

    token = _extract_bearer_token(request)
    if not token:
        raise HTTPException(status_code=401, detail="bearer token required")

    bootstrap = _bootstrap_admin_token()
    if bootstrap and secrets.compare_digest(token, bootstrap):
        identity = {
            "auth_mode": "bootstrap-token",
            "role": "super_admin",
            "scopes": ["*"],
            "actor": _bootstrap_admin_subject(),
            "token_id": "bootstrap",
            "subject": _bootstrap_admin_subject(),
            "user_id": None,
        }
        request.state.identity = identity
        return identity

    if _find_token_record(token):
        token_record = _validate_token(token)
        role = str(token_record.get("role") or "viewer").strip().lower()
        if role not in ROLE_LEVEL:
            role = "viewer"
        scopes = token_record.get("scopes")
        if not isinstance(scopes, list):
            scopes = list(ROLE_PERMISSIONS.get(role, []))
        identity = {
            "auth_mode": "bearer-token",
            "role": role,
            "scopes": [str(scope) for scope in scopes],
            "actor": str(token_record.get("subject") or token_record.get("name") or "token"),
            "token_id": token_record.get("token_id"),
            "subject": str(token_record.get("subject") or ""),
            "user_id": None,
        }
        request.state.identity = identity
        return identity

    user_session = validate_session_token(token)
    if user_session is None:
        raise HTTPException(status_code=401, detail="invalid token")
    role = str(user_session.get("role") or "analyst").strip().lower()
    if role not in ROLE_LEVEL:
        role = "analyst"
    scopes = list(ROLE_PERMISSIONS.get(role, []))
    identity = {
        "auth_mode": "user-session",
        "role": role,
        "scopes": scopes,
        "actor": str(user_session.get("username") or user_session.get("user_id") or "user"),
        "token_id": str(user_session.get("session_id") or ""),
        "subject": str(user_session.get("user_id") or ""),
        "user_id": str(user_session.get("user_id") or ""),
    }
    request.state.identity = identity
    return identity


def _effective_role(request: Request) -> str:
    return str(_effective_identity(request).get("role") or "viewer")


def _has_scope(identity: dict[str, Any], scope: str) -> bool:
    scopes = identity.get("scopes")
    if not isinstance(scopes, list):
        return False
    scope = str(scope or "").strip()
    if not scope:
        return True
    return "*" in scopes or scope in scopes


def _require_role(request: Request, minimum_role: str) -> str:
    identity = _effective_identity(request)
    role = str(identity.get("role") or "viewer")
    if ROLE_LEVEL.get(role, 0) < ROLE_LEVEL.get(minimum_role, 0):
        raise HTTPException(status_code=403, detail=f"{minimum_role}+ role required")
    return role


def _require_scope(request: Request, scope: str) -> None:
    identity = _effective_identity(request)
    if not _has_scope(identity, scope):
        raise HTTPException(status_code=403, detail=f"scope '{scope}' required")


def _actor(request: Request) -> str:
    return str(_effective_identity(request).get("actor") or "system")


def _identity_user_id(request: Request) -> str | None:
    user_id = str(_effective_identity(request).get("user_id") or "").strip()
    return user_id or None


def _optional_identity_user_id(request: Request) -> str | None:
    auth_header = str(request.headers.get("authorization", "")).strip()
    if not auth_header:
        return None
    try:
        return _identity_user_id(request)
    except HTTPException:
        return None


# ---------------------------------------------------------------------------
# Langfuse request helpers
# ---------------------------------------------------------------------------

def _resolve_langfuse_settings_for_request(
    request: Request,
    project_id: int | None = None,
) -> dict[str, Any] | None:
    user_id = _optional_identity_user_id(request)
    if not user_id:
        return None

    settings: dict[str, Any] | None = None

    if project_id is not None:
        proj = get_user_langfuse_project(project_id, user_id=user_id)
        if proj:
            settings = proj
    else:
        # Try default project first, then fall back to legacy settings
        projects = list_user_langfuse_projects(user_id)
        default_proj = next((p for p in projects if p.get("is_default")), None)
        if default_proj:
            settings = default_proj
        else:
            settings = get_user_langfuse_settings(user_id)

    if not settings:
        return None
    host = str(settings.get("base_url") or "").strip()
    public_key = str(settings.get("public_key") or "").strip()
    secret_key = str(settings.get("secret_key") or "").strip()
    environment = str(settings.get("environment") or "").strip()
    if not (host and public_key and secret_key):
        return None
    return {
        "host": host,
        "public_key": public_key,
        "secret_key": secret_key,
        "environment": environment,
    }


def _run_langfuse_operation_for_request(
    request: Request,
    operation: str,
    params: dict[str, Any] | None = None,
) -> dict[str, Any]:
    return run_langfuse_control_operation(
        operation=operation,
        params=params,
        settings=_resolve_langfuse_settings_for_request(request),
    )


def _run_langfuse_message_for_request(request: Request, message: str) -> dict[str, Any]:
    return run_langfuse_control_message(message, settings=_resolve_langfuse_settings_for_request(request))


# ---------------------------------------------------------------------------
# Audit
# ---------------------------------------------------------------------------

def _record_audit(
    request: Request,
    *,
    action: str,
    resource_type: str,
    resource_id: str,
    detail: dict[str, Any] | None = None,
) -> None:
    import json as _json

    identity = _effective_identity(request)
    extra = _json.dumps({
        "role": identity.get("role"),
        "auth_mode": identity.get("auth_mode"),
        "token_id": identity.get("token_id"),
        "resource_type": resource_type,
        "resource_id": resource_id,
    })
    detail_str = _json.dumps(detail) if detail else ""
    ip = str(getattr(request.client, "host", "") if request.client else "")
    insert_audit_log(
        actor=_actor(request),
        action=action,
        detail=detail_str,
        ip=ip,
        extra=extra,
    )


# ---------------------------------------------------------------------------
# Langfuse mutation helpers
# ---------------------------------------------------------------------------

def _is_langfuse_mutation_operation(operation: str) -> bool:
    normalized = str(operation or "").strip().lower()
    return normalized in {
        "prompt.upsert",
        "prompt.delete",
        "prompt.promote_label",
        "datasets.create",
        "dataset.items.upsert",
        "datasets.import_from_traces",
        "score.create",
        "annotation.create",
    }


def _openapi_execute_requires_write(payload: OpenApiExecuteRequest) -> bool:
    if payload.method and str(payload.method).strip().upper() in {"POST", "PUT", "PATCH", "DELETE"}:
        return True
    try:
        return is_langfuse_openapi_mutating(
            operation_id=payload.operation_id,
            method=payload.method,
            path=payload.path,
        )
    except Exception:
        return True


# ---------------------------------------------------------------------------
# Admin helpers
# ---------------------------------------------------------------------------

def _ensure_admin_seed() -> None:
    if USERS:
        return
    user_id = _new_id("user")
    USERS[user_id] = {
        "user_id": user_id,
        "email": "admin@local",
        "name": "Local Admin",
        "roles": ["super_admin"],
        "status": "active",
        "created_at": _now_iso(),
        "updated_at": _now_iso(),
    }


def _normalize_role_and_scopes(role: str, scopes: list[str]) -> tuple[str, list[str]]:
    normalized_role = str(role or "viewer").strip().lower()
    if normalized_role not in ROLE_LEVEL:
        normalized_role = "viewer"
    cleaned_scopes = [str(scope).strip() for scope in scopes if str(scope).strip()]
    if not cleaned_scopes:
        cleaned_scopes = list(ROLE_PERMISSIONS.get(normalized_role, []))
    return normalized_role, cleaned_scopes


# ---------------------------------------------------------------------------
# Token management
# ---------------------------------------------------------------------------

def _issue_token_record(
    *,
    name: str,
    role: str,
    scopes: list[str],
    subject: str,
    expires_days: int,
    created_by: str,
    auto_rotate: bool,
    rotate_before_days: int,
) -> tuple[dict[str, Any], str]:
    token_id = _new_id("tok")
    raw_token = f"lla_{secrets.token_urlsafe(24)}"
    token_hash = hashlib.sha256(raw_token.encode("utf-8")).hexdigest()
    expires_at = (datetime.now(timezone.utc) + timedelta(days=max(1, int(expires_days)))).isoformat().replace("+00:00", "Z")
    normalized_role, normalized_scopes = _normalize_role_and_scopes(role, scopes)
    item = {
        "token_id": token_id,
        "name": str(name or "token").strip() or "token",
        "role": normalized_role,
        "scopes": normalized_scopes,
        "subject": str(subject or "").strip() or str(name or "token").strip() or "token",
        "token_hash": token_hash,
        "created_at": _now_iso(),
        "expires_at": expires_at,
        "revoked": False,
        "created_by": created_by,
        "auto_rotate": bool(auto_rotate),
        "rotate_before_days": max(1, int(rotate_before_days)),
        "last_used_at": None,
        "rotated_from": None,
    }
    with STATE_LOCK:
        TOKENS[token_id] = item
    return item, raw_token


def _token_public_view(record: dict[str, Any]) -> dict[str, Any]:
    return {k: v for k, v in record.items() if k != "token_hash"}


def _token_is_expiring(record: dict[str, Any], within_days: int) -> bool:
    if record.get("revoked"):
        return False
    expires_at = _parse_datetime(record.get("expires_at"))
    if not expires_at:
        return False
    deadline = datetime.now(timezone.utc) + timedelta(days=max(0, int(within_days)))
    return expires_at <= deadline


def _rotate_token_record(token_id: str, *, rotated_by: str, force: bool = False, within_days: int = 3) -> dict[str, Any]:
    with STATE_LOCK:
        current = TOKENS.get(token_id)
        if not current:
            raise HTTPException(status_code=404, detail="token not found")
        snapshot = dict(current)

    if snapshot.get("revoked"):
        raise HTTPException(status_code=400, detail="token already revoked")
    if not force:
        auto_rotate = bool(snapshot.get("auto_rotate", True))
        if not auto_rotate:
            raise HTTPException(status_code=400, detail="token auto-rotation disabled")
        rotate_before_days = int(snapshot.get("rotate_before_days", within_days) or within_days)
        if not _token_is_expiring(snapshot, rotate_before_days):
            raise HTTPException(status_code=400, detail="token is not in rotation window")

    old_expires_at = _parse_datetime(snapshot.get("expires_at"))
    remaining_days = 0
    if old_expires_at is not None:
        remaining_days = (old_expires_at - datetime.now(timezone.utc)).days
    if remaining_days < 1:
        remaining_days = _env_int("AGENT_TOKEN_DEFAULT_EXPIRES_DAYS", 30, 1, 3650)

    new_item, raw_token = _issue_token_record(
        name=str(snapshot.get("name", "token")),
        role=str(snapshot.get("role", "viewer")),
        scopes=list(snapshot.get("scopes") or []),
        subject=str(snapshot.get("subject", "")),
        expires_days=remaining_days,
        created_by=rotated_by,
        auto_rotate=bool(snapshot.get("auto_rotate", True)),
        rotate_before_days=int(snapshot.get("rotate_before_days", 3) or 3),
    )

    with STATE_LOCK:
        old = TOKENS.get(token_id)
        if old is not None:
            old["revoked"] = True
            old["revoked_at"] = _now_iso()
            old["revoke_reason"] = "rotated"
            old["replaced_by"] = new_item["token_id"]
        fresh = TOKENS.get(new_item["token_id"])
        if fresh is not None:
            fresh["rotated_from"] = token_id

    return {"new_record": _token_public_view(new_item), "new_token": raw_token, "old_token_id": token_id}
