import re
import secrets
from datetime import datetime, timedelta, timezone
from sqlalchemy import select, func, text
from sqlalchemy.orm import Session
from app.models.entities import (
    Tenant, TenantBranding, TenantDomain, TenantRuntimeNamespace,
    TenantQuotaReservation, TenantUsageMeter, SaaSSubscription, SaaSPlan,
)

HOST_RE=re.compile(r'^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$')

def normalize_hostname(value:str)->str:
    host=(value or '').split(':',1)[0].strip().lower().rstrip('.')
    if not HOST_RE.match(host): raise ValueError('Hostname inválido')
    return host

def resolve_tenant(db:Session,tenant_id:str='',hostname:str='')->Tenant|None:
    if tenant_id:
        tenant=db.get(Tenant,tenant_id)
    else:
        host=normalize_hostname(hostname)
        domain=db.scalar(select(TenantDomain).where(TenantDomain.hostname==host,TenantDomain.verified.is_(True)))
        if not domain:
            branding=db.scalar(select(TenantBranding).where(TenantBranding.custom_domain==host))
            tenant=db.get(Tenant,branding.tenant_id) if branding else None
        else: tenant=db.get(Tenant,domain.tenant_id)
    return tenant if tenant and tenant.status=='active' else None

def apply_postgres_tenant_context(db:Session,tenant_id:str,user_id:str='')->None:
    # PostgreSQL RLS policies consume transaction-local settings. No SQL interpolation is used.
    if db.bind and db.bind.dialect.name=='postgresql':
        db.execute(text("SELECT set_config('app.current_tenant_id', :tenant_id, true)"),{'tenant_id':tenant_id})
        db.execute(text("SELECT set_config('app.current_user_id', :user_id, true)"),{'user_id':user_id or ''})

def ensure_namespace(db:Session,tenant:Tenant)->TenantRuntimeNamespace:
    item=db.scalar(select(TenantRuntimeNamespace).where(TenantRuntimeNamespace.tenant_id==tenant.id))
    if item: return item
    safe=re.sub(r'[^a-z0-9_]+','_',tenant.slug.lower()).strip('_')[:48] or tenant.id.replace('-','')[:20]
    item=TenantRuntimeNamespace(tenant_id=tenant.id,redis_prefix=f'tenant:{tenant.id}',
        celery_queue=f'tenant.{safe}',object_prefix=f'tenants/{tenant.id}/',
        schema_name=f'tenant_{safe}' if tenant.isolation_mode=='schema_per_tenant' else 'public',
        database_alias=f'tenant_{safe}' if tenant.isolation_mode=='database_per_tenant' else 'metadata',
        isolation_mode=tenant.isolation_mode)
    db.add(item); db.commit(); db.refresh(item); return item

def tenant_redis_key(namespace:TenantRuntimeNamespace,key:str)->str:
    return f"{namespace.redis_prefix}:{key.lstrip(':')}"

def tenant_object_key(namespace:TenantRuntimeNamespace,key:str)->str:
    clean=key.lstrip('/').replace('..','_')
    return f"{namespace.object_prefix}{clean}"

def tenant_queue(namespace:TenantRuntimeNamespace,priority:str='default')->str:
    priority=priority if priority in {'low','default','high'} else 'default'
    return f'{namespace.celery_queue}.{priority}'

def register_domain(db:Session,tenant_id:str,hostname:str,primary:bool=False)->TenantDomain:
    host=normalize_hostname(hostname)
    existing=db.scalar(select(TenantDomain).where(TenantDomain.hostname==host))
    if existing and existing.tenant_id!=tenant_id: raise ValueError('Domínio já pertence a outro tenant')
    item=existing or TenantDomain(tenant_id=tenant_id,hostname=host,verification_token=secrets.token_urlsafe(32))
    if primary:
        for row in db.scalars(select(TenantDomain).where(TenantDomain.tenant_id==tenant_id,TenantDomain.is_primary.is_(True))): row.is_primary=False
    item.is_primary=primary; db.add(item); db.commit(); db.refresh(item); return item

def verify_domain(db:Session,tenant_id:str,domain_id:str,token:str)->TenantDomain:
    item=db.scalar(select(TenantDomain).where(TenantDomain.id==domain_id,TenantDomain.tenant_id==tenant_id))
    if not item or not secrets.compare_digest(item.verification_token,token): raise ValueError('Verificação inválida')
    item.verified=True; item.tls_status='ready'; item.verified_at=datetime.now(timezone.utc)
    db.add(item); db.commit(); db.refresh(item); return item

def quota_limit(db:Session,tenant_id:str,metric_code:str)->int|None:
    sub=db.scalar(select(SaaSSubscription).where(SaaSSubscription.tenant_id==tenant_id,SaaSSubscription.status.in_(['trialing','active'])))
    if not sub: return 0
    plan=db.get(SaaSPlan,sub.plan_id)
    limits=(plan.limits_json or {}) if plan else {}
    value=limits.get(metric_code)
    return int(value) if value is not None else None

def reserve_quota(db:Session,tenant_id:str,metric_code:str,quantity:int,idempotency_key:str,
                  user_id:str|None=None,reference_type:str='',reference_id:str='',ttl_seconds:int=900)->TenantQuotaReservation:
    if quantity<=0: raise ValueError('Quantidade deve ser positiva')
    existing=db.scalar(select(TenantQuotaReservation).where(TenantQuotaReservation.idempotency_key==idempotency_key))
    if existing:
        if existing.tenant_id!=tenant_id or existing.metric_code!=metric_code or existing.quantity!=quantity:
            raise ValueError('Chave de idempotência reutilizada com dados diferentes')
        return existing
    limit=quota_limit(db,tenant_id,metric_code)
    used=db.scalar(select(func.coalesce(func.sum(TenantUsageMeter.quantity),0)).where(
        TenantUsageMeter.tenant_id==tenant_id,TenantUsageMeter.metric_code==metric_code)) or 0
    reserved=db.scalar(select(func.coalesce(func.sum(TenantQuotaReservation.quantity),0)).where(
        TenantQuotaReservation.tenant_id==tenant_id,TenantQuotaReservation.metric_code==metric_code,
        TenantQuotaReservation.status=='reserved')) or 0
    if limit is not None and used+reserved+quantity>limit: raise PermissionError('Quota contratada excedida')
    item=TenantQuotaReservation(tenant_id=tenant_id,metric_code=metric_code,quantity=quantity,
        idempotency_key=idempotency_key,reference_type=reference_type,reference_id=reference_id,
        expires_at=datetime.now(timezone.utc)+timedelta(seconds=max(60,ttl_seconds)),created_by=user_id)
    db.add(item); db.commit(); db.refresh(item); return item

def finalize_quota(db:Session,item:TenantQuotaReservation,consumed:bool=True)->TenantQuotaReservation:
    if item.status!='reserved': return item
    item.status='consumed' if consumed else 'released'; item.finalized_at=datetime.now(timezone.utc)
    if consumed: db.add(TenantUsageMeter(tenant_id=item.tenant_id,metric_code=item.metric_code,quantity=item.quantity,unit='unit',dimensions={'reservation_id':item.id}))
    db.add(item); db.commit(); db.refresh(item); return item
