from __future__ import annotations
import re, secrets
from datetime import datetime, timezone, timedelta
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.core.security.passwords import hash_password
from app.core.settings import get_settings
from app.modules.clients.models import Client
from app.modules.core.iam.invite_service import issue_invite_token
from app.modules.core.iam.models import User
from app.modules.core.rbac.models import Role, UserRole
from app.modules.email_integration.services import send_user_invite_email
from .models import ClientIdentityAuditLog, ClientPortalIdentity, ClientPortalInvitation

PAN_RE = re.compile(r"^[A-Z]{5}[0-9]{4}[A-Z]$")

def normalize_pan(value: str | None) -> str:
    pan = re.sub(r"\s+", "", (value or "").upper())
    if not PAN_RE.fullmatch(pan):
        raise ValueError("A valid 10-character PAN is required for client portal identity.")
    return pan

def normalize_email(value: str | None) -> str:
    email = (value or "").strip().lower()
    if not email or "@" not in email or len(email) > 255:
        raise ValueError("A valid email address is required.")
    return email

def placeholder_email(tenant_id: int, pan: str) -> str:
    return f"client+{tenant_id}.{pan.lower()}@invalid.local"

def _audit(db: Session, identity: ClientPortalIdentity, actor_user_id: int | None, action: str, summary: str):
    db.add(ClientIdentityAuditLog(tenant_id=identity.tenant_id, client_id=identity.client_id, identity_id=identity.id, actor_user_id=actor_user_id, action=action, summary=summary))

def _ensure_client_role(db: Session, user_id: int):
    role = db.execute(select(Role).where(Role.name == "Client")).scalar_one_or_none()
    if not role:
        raise ValueError("Client role is not configured in RBAC.")
    exists = db.execute(select(UserRole).where(UserRole.user_id == user_id, UserRole.role_id == role.id)).scalar_one_or_none()
    if not exists:
        db.add(UserRole(user_id=user_id, role_id=role.id))

def get_identity(db: Session, client_id: int) -> ClientPortalIdentity | None:
    return db.execute(select(ClientPortalIdentity).where(ClientPortalIdentity.client_id == client_id)).scalar_one_or_none()

def ensure_identity(db: Session, *, client: Client, actor_user_id: int | None, requested_email: str | None = None) -> ClientPortalIdentity:
    pan = normalize_pan(client.pan)
    existing_pan = db.execute(select(ClientPortalIdentity).where(ClientPortalIdentity.tenant_id == client.tenant_id, ClientPortalIdentity.normalized_pan == pan, ClientPortalIdentity.client_id != client.id)).scalar_one_or_none()
    if existing_pan:
        raise ValueError("This PAN is already linked to another client portal identity in the firm.")
    identity = get_identity(db, client.id)
    real_email = (requested_email or client.email or "").strip().lower()
    is_placeholder = not bool(real_email)
    email = placeholder_email(client.tenant_id, pan) if is_placeholder else normalize_email(real_email)
    other_user = db.execute(select(User).where(User.email == email)).scalar_one_or_none()
    if other_user and (not identity or identity.user_id != other_user.id) and client.portal_user_id != other_user.id:
        raise ValueError("This email address is already used by another ERP user.")
    if identity:
        identity.normalized_pan = pan
        identity.updated_by_user_id = actor_user_id
        if identity.user_id:
            user = db.get(User, identity.user_id)
            if user and user.email != email:
                user.email = email
        identity.login_email = email
        identity.email_is_placeholder = is_placeholder
    else:
        user = db.get(User, client.portal_user_id) if client.portal_user_id else None
        if not user:
            user = User(email=email, full_name=client.client_name, password_hash=hash_password(secrets.token_urlsafe(48)), tenant_id=client.tenant_id, branch_id=client.branch_id, is_active=True, allow_login=False, must_change_password=True)
            db.add(user); db.flush()
            _ensure_client_role(db, user.id)
            client.portal_user_id = user.id
        identity = ClientPortalIdentity(tenant_id=client.tenant_id, branch_id=client.branch_id, client_id=client.id, user_id=user.id, normalized_pan=pan, login_email=email, email_is_placeholder=is_placeholder, status="not_invited", created_by_user_id=actor_user_id, updated_by_user_id=actor_user_id)
        db.add(identity); db.flush()
        _audit(db, identity, actor_user_id, "identity_created", "PAN-linked client portal identity created.")
    db.commit(); db.refresh(identity)
    return identity

def update_login_email(db: Session, *, identity: ClientPortalIdentity, email: str, actor_user_id: int | None):
    new_email = normalize_email(email)
    other = db.execute(select(User).where(User.email == new_email, User.id != identity.user_id)).scalar_one_or_none()
    if other: raise ValueError("This email address is already used by another ERP user.")
    client = db.get(Client, identity.client_id); user = db.get(User, identity.user_id) if identity.user_id else None
    old = identity.login_email
    identity.login_email = new_email; identity.email_is_placeholder = False; identity.updated_by_user_id = actor_user_id
    if client: client.email = new_email
    if user: user.email = new_email
    _audit(db, identity, actor_user_id, "email_changed", f"Portal login email changed from {old} to {new_email}.")
    db.commit()

def issue_client_invitation(db: Session, *, identity: ClientPortalIdentity, actor_user_id: int | None, send_email: bool = True) -> tuple[str, ClientPortalInvitation]:
    user = db.get(User, identity.user_id) if identity.user_id else None
    if not user: raise ValueError("Client portal user is not linked.")
    token = issue_invite_token(db, user)
    now = datetime.now(timezone.utc)
    user.allow_login = False
    identity.status = "invited"; identity.invited_at_utc = identity.invited_at_utc or now; identity.last_invited_at_utc = now; identity.updated_by_user_id = actor_user_id
    mode = "manual_link" if identity.email_is_placeholder or not send_email else "email"
    row = ClientPortalInvitation(identity_id=identity.id, user_id=user.id, delivery_mode=mode, recipient_email=None if identity.email_is_placeholder else identity.login_email, status="issued", issued_by_user_id=actor_user_id, issued_at_utc=now, expires_at_utc=now + timedelta(hours=get_settings().INVITE_TOKEN_HOURS))
    db.add(row); _audit(db, identity, actor_user_id, "invite_issued", f"Client portal invitation issued using {mode}.")
    if mode == "email":
        try: send_user_invite_email(db, user=user, invite_token=token)
        except Exception as exc: row.note = f"Email delivery failed: {exc}"; row.delivery_mode = "manual_link"
    db.commit(); db.refresh(row)
    return token, row

def resolve_login_user(db: Session, identifier: str, bound_tenant_id: int | None = None) -> User | None:
    value = (identifier or "").strip()
    if "@" in value:
        return db.execute(select(User).where(User.email == value.lower())).scalar_one_or_none()
    try: pan = normalize_pan(value)
    except ValueError: return None
    q = select(ClientPortalIdentity).where(ClientPortalIdentity.normalized_pan == pan)
    if bound_tenant_id is not None: q = q.where(ClientPortalIdentity.tenant_id == bound_tenant_id)
    rows = list(db.execute(q).scalars().all())
    if len(rows) != 1: return None
    return db.get(User, rows[0].user_id) if rows[0].user_id else None

def mark_identity_activated(db: Session, user_id: int):
    identity = db.execute(select(ClientPortalIdentity).where(ClientPortalIdentity.user_id == user_id)).scalar_one_or_none()
    if identity and identity.status != "active":
        now = datetime.now(timezone.utc); identity.status="active"; identity.activated_at_utc=now
        inv = db.execute(select(ClientPortalInvitation).where(ClientPortalInvitation.identity_id==identity.id, ClientPortalInvitation.status=="issued").order_by(ClientPortalInvitation.id.desc())).scalars().first()
        if inv: inv.status="accepted"; inv.accepted_at_utc=now
        _audit(db, identity, user_id, "identity_activated", "Client accepted invitation and activated portal identity.")
        db.commit()
