"""
Invitation flow — org admins invite users; users accept via a public token URL.

Public endpoints (no auth):
  POST /invitations/preview   — inspect token without accepting
  POST /invitations/accept    — accept an invitation

Protected endpoints:
  POST   /organizations/:id/invitations          — create (org ADMIN+)
  GET    /organizations/:id/invitations          — list (org ADMIN+)
  DELETE /organizations/:id/invitations/:inv_id  — revoke (org ADMIN+)
"""

import hashlib
import re
import secrets
from datetime import UTC, datetime, timedelta

from fastapi import APIRouter, Depends, HTTPException, Request
from motor.motor_asyncio import AsyncIOMotorDatabase

from ...core.authz import bump_user_membership_cache
from ...core.database import get_database
from ...core.dependencies import get_current_user
from ...core.security import (
    create_access_token,
    create_refresh_token,
    get_password_hash,
)
from ...models.audit_log import AuditAction
from ...models.invitation import (
    InvitationAcceptRequest,
    InvitationCreate,
    InvitationPreviewResponse,
    InvitationResponse,
)
from ...models.organization import OrgRole
from ...models.user import AuthProvider, User, UserRole
from ...services.audit_logger import audit_logger
from ...services.emailer import email_service
from ...services.membership_service import get_membership, upsert_membership

router = APIRouter(tags=["invitations"])


def _now() -> datetime:
    return datetime.now(UTC)


def _hash_token(plaintext: str) -> str:
    return hashlib.sha256(plaintext.encode()).hexdigest()


def _make_token() -> tuple[str, str]:
    """Return (plaintext, sha256_hash) for a new invitation token."""
    plaintext = secrets.token_urlsafe(32)
    return plaintext, _hash_token(plaintext)


def _inv_from_doc(doc: dict) -> InvitationResponse:
    now = _now()
    expires_at = doc["expires_at"].replace(tzinfo=UTC) if doc["expires_at"].tzinfo is None else doc["expires_at"]
    return InvitationResponse(
        id=str(doc["_id"]),
        email=doc["email"],
        organization_id=doc["organization_id"],
        role_in_org=OrgRole(doc["role_in_org"]),
        invited_by_user_id=doc["invited_by_user_id"],
        expires_at=expires_at,
        accepted_at=doc.get("accepted_at"),
        revoked_at=doc.get("revoked_at"),
        created_at=doc.get("created_at"),
        is_expired=expires_at < now and doc.get("accepted_at") is None,
        is_accepted=doc.get("accepted_at") is not None,
        is_revoked=doc.get("revoked_at") is not None,
    )


async def _get_org_name(org_id: str, db: AsyncIOMotorDatabase) -> tuple[str, str]:
    """Return (name, slug) for an org, checking both collections."""
    doc = await db.organizations.find_one({"_id": org_id})
    if not doc:
        doc = await db.clients.find_one({"_id": org_id})
    if not doc:
        return (org_id, org_id)
    return doc["name"], doc["slug"]


async def _assert_org_admin(org_id: str, user: User, db: AsyncIOMotorDatabase) -> None:
    if user.role == UserRole.ADMIN:
        return
    membership = await get_membership(user.id, org_id, db)
    if not membership or membership.role_in_org < OrgRole.ADMIN:
        raise HTTPException(status_code=403, detail="Org ADMIN role required")


# ---------------------------------------------------------------------------
# Org-scoped invitation endpoints (mounted on the organizations router)
# ---------------------------------------------------------------------------

org_router = APIRouter(prefix="/organizations", tags=["invitations"])


@org_router.post("/{org_id}/invitations", response_model=InvitationResponse, status_code=201)
async def create_invitation(
    org_id: str,
    body: InvitationCreate,
    request: Request,
    current_user: User = Depends(get_current_user),
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    await _assert_org_admin(org_id, current_user, db)

    email_lower = body.email.lower()

    # Check for existing pending invitation to the same email+org
    existing = await db.invitations.find_one({
        "email": email_lower,
        "organization_id": org_id,
        "accepted_at": None,
        "revoked_at": None,
        "expires_at": {"$gt": _now()},
    })
    if existing:
        raise HTTPException(
            status_code=409,
            detail="A pending invitation already exists for this email. Revoke it first to re-invite.",
        )

    # MT-19: ensure all target_team_ids belong to this org (client_id == org_id)
    if body.target_team_ids:
        valid_teams = await db.teams.count_documents({
            "_id": {"$in": body.target_team_ids},
            "client_id": org_id,
        })
        if valid_teams != len(body.target_team_ids):
            raise HTTPException(
                status_code=400,
                detail="One or more target_team_ids do not belong to this organization.",
            )

    plaintext, token_hash = _make_token()
    now = _now()
    expires_at = now + timedelta(days=body.expires_in_days)

    from bson import ObjectId
    doc = {
        "_id": str(ObjectId()),
        "email": email_lower,
        "organization_id": org_id,
        "role_in_org": body.role_in_org.value,
        "target_team_ids": body.target_team_ids,
        "token_hash": token_hash,
        "invited_by_user_id": current_user.id,
        "expires_at": expires_at,
        "accepted_at": None,
        "revoked_at": None,
        "created_at": now,
    }
    await db.invitations.insert_one(doc)

    org_name, org_slug = await _get_org_name(org_id, db)
    from ...core.config import settings
    base_url = settings.cors_origins_list[0].rstrip("/") if settings.cors_origins_list else "https://app.example.com"
    accept_url = f"{base_url}/video-accessibility/accept-invite?token={plaintext}"

    await email_service.send_invitation_email(
        to_email=email_lower,
        inviter_name=current_user.full_name,
        org_name=org_name,
        accept_url=accept_url,
        expires_at=expires_at,
    )

    inv = _inv_from_doc(doc)
    await audit_logger.log_action(
        action=AuditAction.INVITATION_CREATE,
        description=f"Invitation created for '{email_lower}' to organization '{org_id}'",
        user=current_user,
        request=request,
        resource_type="invitation",
        resource_id=inv.id,
        details={"invited_email": email_lower, "org_id": org_id, "role": body.role_in_org},
    )
    return inv


@org_router.get("/{org_id}/invitations", response_model=list[InvitationResponse])
async def list_invitations(
    org_id: str,
    current_user: User = Depends(get_current_user),
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    await _assert_org_admin(org_id, current_user, db)
    docs = []
    async for doc in db.invitations.find({"organization_id": org_id}).sort("created_at", -1):
        docs.append(_inv_from_doc(doc))
    return docs


@org_router.delete("/{org_id}/invitations/{invitation_id}", status_code=204)
async def revoke_invitation(
    org_id: str,
    invitation_id: str,
    request: Request,
    current_user: User = Depends(get_current_user),
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    await _assert_org_admin(org_id, current_user, db)
    inv_doc = await db.invitations.find_one({"_id": invitation_id, "organization_id": org_id})
    result = await db.invitations.update_one(
        {"_id": invitation_id, "organization_id": org_id, "accepted_at": None, "revoked_at": None},
        {"$set": {"revoked_at": _now()}},
    )
    if result.matched_count == 0:
        raise HTTPException(status_code=404, detail="Invitation not found or already accepted/revoked")
    await audit_logger.log_action(
        action=AuditAction.INVITATION_REVOKE,
        description=f"Invitation '{invitation_id}' revoked in organization '{org_id}'",
        user=current_user,
        request=request,
        resource_type="invitation",
        resource_id=invitation_id,
        details={
            "invited_email": inv_doc["email"] if inv_doc else None,
            "org_id": org_id,
        },
    )


# ---------------------------------------------------------------------------
# Public endpoints (no auth required)
# ---------------------------------------------------------------------------

@router.post("/invitations/preview", response_model=InvitationPreviewResponse)
async def preview_invitation(
    body: dict,
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    """Inspect an invitation token without consuming it."""
    token = body.get("token", "")
    if not token:
        raise HTTPException(status_code=400, detail="token is required")

    token_hash = _hash_token(token)
    doc = await db.invitations.find_one({"token_hash": token_hash})
    if not doc:
        raise HTTPException(status_code=410, detail="Invitation not found or has expired")

    now = _now()
    expires_at = doc["expires_at"].replace(tzinfo=UTC) if doc["expires_at"].tzinfo is None else doc["expires_at"]

    if doc.get("revoked_at"):
        raise HTTPException(status_code=410, detail="This invitation has been revoked")
    if doc.get("accepted_at"):
        raise HTTPException(status_code=409, detail="This invitation has already been accepted")
    if expires_at < now:
        raise HTTPException(status_code=410, detail="This invitation has expired. Ask your admin to re-send.")

    org_name, org_slug = await _get_org_name(doc["organization_id"], db)

    # Find inviter name
    inviter = await db.users.find_one({"_id": doc["invited_by_user_id"]})
    inviter_name = inviter["full_name"] if inviter else "Your admin"

    # Check if user already exists
    email_lower = doc["email"]
    existing_user = await db.users.find_one(
        {"email": {"$regex": f"^{re.escape(email_lower)}$", "$options": "i"}}
    )

    # Check if already a member
    is_existing_member = False
    if existing_user:
        mem = await get_membership(str(existing_user["_id"]), doc["organization_id"], db)
        is_existing_member = mem is not None

    requires_password_setup = existing_user is None
    requires_microsoft_signin = (
        existing_user is not None
        and existing_user.get("auth_provider") == "microsoft"
        and existing_user.get("hashed_password") is None
    )

    return InvitationPreviewResponse(
        org_name=org_name,
        org_slug=org_slug,
        inviter_name=inviter_name,
        role_in_org=OrgRole(doc["role_in_org"]),
        target_email=email_lower,
        requires_password_setup=requires_password_setup,
        requires_microsoft_signin=requires_microsoft_signin,
        is_existing_member=is_existing_member,
    )


@router.post("/invitations/accept")
async def accept_invitation(
    body: InvitationAcceptRequest,
    request: Request,
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    """Accept an invitation. Creates user if needed, creates membership, returns tokens."""
    token_hash = _hash_token(body.token)
    now = _now()

    # Atomically mark the invitation as accepted (single-use)
    doc = await db.invitations.find_one_and_update(
        {
            "token_hash": token_hash,
            "accepted_at": None,
            "revoked_at": None,
            "expires_at": {"$gt": now},
        },
        {"$set": {"accepted_at": now}},
        return_document=True,
    )
    if not doc:
        raise HTTPException(
            status_code=410,
            detail="Invitation not found, already accepted, revoked, or expired",
        )

    email_lower = doc["email"]
    org_id = doc["organization_id"]
    role_in_org = OrgRole(doc["role_in_org"])

    # Find or create user
    existing_user = await db.users.find_one(
        {"email": {"$regex": f"^{re.escape(email_lower)}$", "$options": "i"}}
    )

    if existing_user:
        user_id = str(existing_user["_id"])
    else:
        # New user: require full_name + password
        if not body.full_name or not body.password:
            raise HTTPException(
                status_code=422,
                detail="full_name and password are required for new accounts",
            )
        from bson import ObjectId
        user_id = str(ObjectId())
        new_user = {
            "_id": user_id,
            "email": email_lower,
            "full_name": body.full_name,
            "hashed_password": get_password_hash(body.password),
            "role": UserRole.CLIENT.value,
            "auth_provider": AuthProvider.LOCAL.value,
            "is_active": True,
            "pm_client_ids": [],
            "created_at": now,
            "updated_at": now,
        }
        await db.users.insert_one(new_user)
        existing_user = new_user

    # Create or upgrade membership
    await upsert_membership(user_id, org_id, role_in_org, doc["invited_by_user_id"], db)
    await bump_user_membership_cache(user_id)

    # Auto-add to target teams — write to both Team.member_user_ids (legacy) and Membership.team_ids (MT-17)
    for team_id in doc.get("target_team_ids", []):
        await db.teams.update_one(
            {"_id": team_id, "client_id": org_id},
            {"$addToSet": {"member_user_ids": user_id}},
        )
        await db.memberships.update_one(
            {"user_id": user_id, "organization_id": org_id},
            {"$addToSet": {"team_ids": team_id}},
        )

    # Send welcome email
    if not existing_user.get("_welcomed"):
        org_name, org_slug = await _get_org_name(org_id, db)
        await email_service.send_welcome_email(
            to_email=email_lower,
            full_name=existing_user.get("full_name", ""),
            org_name=org_name,
        )

    # Issue JWT tokens with org_ids claim
    _inv_org_ids = [m["organization_id"] async for m in db.memberships.find({"user_id": user_id}, {"organization_id": 1})]
    access_token = create_access_token(subject=user_id, org_ids=[str(o) for o in _inv_org_ids if o])
    refresh_token = create_refresh_token(subject=user_id)

    org_name, org_slug = await _get_org_name(org_id, db)

    await audit_logger.log_action(
        action=AuditAction.INVITATION_ACCEPT,
        description=f"Invitation accepted by '{email_lower}' for organization '{org_id}'",
        user=None,
        request=request,
        resource_type="invitation",
        resource_id=str(doc["_id"]),
        details={"invited_email": email_lower, "org_id": org_id},
    )

    return {
        "access_token": access_token,
        "refresh_token": refresh_token,
        "user_id": user_id,
        "email": email_lower,
        "full_name": existing_user.get("full_name", ""),
        "redirect_to": f"/org/{org_slug}/dashboard",
    }
