import hashlib, json
from datetime import datetime, timezone
from typing import Any
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.api.deps import current_user
from app.core.database import get_db
from app.core.security import encrypt_secret
from app.models.entities import (
    User, Notification, WorkflowTask, AlertEvent, CommunicationChannel, ChannelMember,
    MobileDevice, PushSubscription, MobileNotificationPreference, OfflineSyncOperation,
    MobileSyncCursor, ChannelMessage
)

router=APIRouter(prefix='/api/v010',tags=['V010 Mobile'])
def now(): return datetime.now(timezone.utc)

class DeviceIn(BaseModel):
    device_key:str=Field(min_length=8,max_length=180)
    platform:str='pwa'; device_name:str=''; app_version:str='1.0.0'
    locale:str='pt-BR'; timezone:str='America/Sao_Paulo'; capabilities:dict={}
class PushIn(BaseModel): device_id:str|None=None; subscription:dict
class PrefIn(BaseModel):
    push_enabled:bool=True; mentions:bool=True; direct_messages:bool=True
    workflow_tasks:bool=True; alerts:bool=True; reports:bool=True
    quiet_hours_start:str='22:00'; quiet_hours_end:str='07:00'
class SyncOperationIn(BaseModel):
    client_operation_id:str=Field(min_length=8,max_length=100)
    operation_type:str
    payload:dict={}
    client_created_at:datetime|None=None
class SyncBatchIn(BaseModel): device_id:str; operations:list[SyncOperationIn]=[]; cursors:dict[str,str]={}

def owned_device(db:Session,device_id:str,u:User):
    d=db.get(MobileDevice,device_id)
    if not d or d.user_id!=u.id or not d.active: raise HTTPException(404,'Dispositivo não encontrado')
    return d

def serialize_notification(n:Notification):
    return {'id':n.id,'title':n.title,'body':n.body,'level':n.level,'link':n.link,'read_at':n.read_at,'created_at':n.created_at}

@router.get('/bootstrap')
def bootstrap(db:Session=Depends(get_db),u:User=Depends(current_user)):
    notifications=db.scalars(select(Notification).where(Notification.user_id==u.id).order_by(Notification.created_at.desc()).limit(30)).all()
    tasks=db.scalars(select(WorkflowTask).where(WorkflowTask.status=='open').order_by(WorkflowTask.created_at.desc()).limit(30)).all()
    visible_tasks=[t for t in tasks if t.assignee_id==u.id or t.assignee_role==u.role.value or (t.assignee_department_id and t.assignee_department_id==u.department_id)]
    channels=db.scalars(select(CommunicationChannel).where(CommunicationChannel.active.is_(True)).order_by(CommunicationChannel.name).limit(100)).all()
    member_ids=set(db.scalars(select(ChannelMember.channel_id).where(ChannelMember.user_id==u.id)).all())
    channels=[c for c in channels if c.visibility=='public' or c.owner_id==u.id or c.id in member_ids or (c.visibility=='department' and c.department_id==u.department_id)]
    unread=sum(1 for n in notifications if not n.read_at)
    return {'version':'V010','user':{'id':u.id,'name':u.full_name,'role':u.role.value,'locale':u.locale,'timezone':u.timezone},'summary':{'unread_notifications':unread,'open_tasks':len(visible_tasks),'channels':len(channels)},'notifications':[serialize_notification(n) for n in notifications[:10]],'tasks':[{'id':t.id,'title':t.title,'due_at':t.due_at,'instance_id':t.instance_id} for t in visible_tasks[:10]],'channels':[{'id':c.id,'name':c.name,'visibility':c.visibility} for c in channels[:20]]}

@router.post('/devices')
def register_device(p:DeviceIn,db:Session=Depends(get_db),u:User=Depends(current_user)):
    d=db.scalar(select(MobileDevice).where(MobileDevice.user_id==u.id,MobileDevice.device_key==p.device_key))
    if not d: d=MobileDevice(user_id=u.id,device_key=p.device_key)
    for k,v in p.model_dump().items(): setattr(d,k,v)
    d.active=True; d.last_seen_at=now(); db.add(d); db.commit(); db.refresh(d)
    return {'id':d.id,'active':d.active,'last_seen_at':d.last_seen_at}

@router.get('/devices')
def devices(db:Session=Depends(get_db),u:User=Depends(current_user)):
    rows=db.scalars(select(MobileDevice).where(MobileDevice.user_id==u.id).order_by(MobileDevice.last_seen_at.desc())).all()
    return [{'id':x.id,'device_name':x.device_name,'platform':x.platform,'app_version':x.app_version,'active':x.active,'last_seen_at':x.last_seen_at} for x in rows]

@router.delete('/devices/{device_id}')
def revoke_device(device_id:str,db:Session=Depends(get_db),u:User=Depends(current_user)):
    d=owned_device(db,device_id,u); d.active=False
    for s in db.scalars(select(PushSubscription).where(PushSubscription.device_id==d.id)).all(): s.active=False
    db.commit(); return {'status':'revoked'}

@router.post('/push/subscriptions')
def subscribe_push(p:PushIn,db:Session=Depends(get_db),u:User=Depends(current_user)):
    if p.device_id: owned_device(db,p.device_id,u)
    endpoint=str(p.subscription.get('endpoint',''))
    if not endpoint.startswith('https://'): raise HTTPException(422,'Endpoint push inválido')
    endpoint_hash=hashlib.sha256(endpoint.encode()).hexdigest()
    s=db.scalar(select(PushSubscription).where(PushSubscription.endpoint_hash==endpoint_hash))
    if s and s.user_id!=u.id: raise HTTPException(409,'Assinatura já vinculada')
    encrypted=encrypt_secret(json.dumps(p.subscription,separators=(',',':')))
    if not s: s=PushSubscription(user_id=u.id,device_id=p.device_id,endpoint_hash=endpoint_hash,encrypted_subscription=encrypted)
    else: s.device_id=p.device_id; s.encrypted_subscription=encrypted; s.active=True; s.failure_count=0
    db.add(s); db.commit(); return {'id':s.id,'active':s.active}

@router.delete('/push/subscriptions/{subscription_id}')
def unsubscribe_push(subscription_id:str,db:Session=Depends(get_db),u:User=Depends(current_user)):
    s=db.get(PushSubscription,subscription_id)
    if not s or s.user_id!=u.id: raise HTTPException(404,'Assinatura não encontrada')
    s.active=False; db.commit(); return {'status':'disabled'}

@router.get('/preferences')
def get_preferences(db:Session=Depends(get_db),u:User=Depends(current_user)):
    p=db.get(MobileNotificationPreference,u.id)
    if not p: p=MobileNotificationPreference(user_id=u.id); db.add(p); db.commit(); db.refresh(p)
    return {k:getattr(p,k) for k in ['push_enabled','mentions','direct_messages','workflow_tasks','alerts','reports','quiet_hours_start','quiet_hours_end']}

@router.put('/preferences')
def set_preferences(body:PrefIn,db:Session=Depends(get_db),u:User=Depends(current_user)):
    p=db.get(MobileNotificationPreference,u.id) or MobileNotificationPreference(user_id=u.id)
    for k,v in body.model_dump().items(): setattr(p,k,v)
    p.updated_at=now(); db.add(p); db.commit(); return body.model_dump()

@router.get('/notifications')
def mobile_notifications(after:datetime|None=None,limit:int=50,db:Session=Depends(get_db),u:User=Depends(current_user)):
    q=select(Notification).where(Notification.user_id==u.id)
    if after: q=q.where(Notification.created_at>after)
    rows=db.scalars(q.order_by(Notification.created_at.desc()).limit(min(max(limit,1),200))).all()
    return {'items':[serialize_notification(x) for x in rows],'server_time':now()}

@router.post('/notifications/{notification_id}/read')
def mark_read(notification_id:str,db:Session=Depends(get_db),u:User=Depends(current_user)):
    n=db.get(Notification,notification_id)
    if not n or n.user_id!=u.id: raise HTTPException(404,'Notificação não encontrada')
    n.read_at=n.read_at or now(); db.commit(); return {'status':'read','read_at':n.read_at}

def execute_operation(db:Session,u:User,op:OfflineSyncOperation)->dict[str,Any]:
    p=op.payload or {}
    if op.operation_type=='notification.read':
        n=db.get(Notification,p.get('notification_id'))
        if not n or n.user_id!=u.id: raise ValueError('Notificação não encontrada')
        n.read_at=n.read_at or now(); return {'notification_id':n.id,'read_at':n.read_at.isoformat()}
    if op.operation_type=='message.create':
        channel_id=str(p.get('channel_id','')); content=str(p.get('content','')).strip()
        c=db.get(CommunicationChannel,channel_id)
        if not c or not content: raise ValueError('Canal ou conteúdo inválido')
        member=db.scalar(select(ChannelMember).where(ChannelMember.channel_id==channel_id,ChannelMember.user_id==u.id))
        allowed=c.visibility=='public' or c.owner_id==u.id or bool(member) or (c.visibility=='department' and c.department_id==u.department_id)
        if not allowed: raise ValueError('Sem acesso ao canal')
        m=ChannelMessage(channel_id=channel_id,sender_id=u.id,content=content[:10000],mentions=[],attachments=[]); db.add(m); db.flush()
        return {'message_id':m.id,'created_at':m.created_at.isoformat()}
    if op.operation_type=='presence.set':
        return {'accepted':True,'status':str(p.get('status','online'))}
    raise ValueError('Tipo de operação offline não suportado')

@router.post('/sync')
def sync(batch:SyncBatchIn,db:Session=Depends(get_db),u:User=Depends(current_user)):
    d=owned_device(db,batch.device_id,u); d.last_seen_at=now(); results=[]
    for incoming in batch.operations[:200]:
        op=db.scalar(select(OfflineSyncOperation).where(OfflineSyncOperation.user_id==u.id,OfflineSyncOperation.client_operation_id==incoming.client_operation_id))
        if op:
            results.append({'client_operation_id':op.client_operation_id,'status':op.status,'result':op.result,'error':op.error}); continue
        op=OfflineSyncOperation(user_id=u.id,device_id=d.id,client_operation_id=incoming.client_operation_id,operation_type=incoming.operation_type,payload=incoming.payload,client_created_at=incoming.client_created_at)
        db.add(op); db.flush()
        try:
            op.result=execute_operation(db,u,op); op.status='processed'; op.processed_at=now()
        except Exception as exc:
            op.status='failed'; op.error=str(exc)[:1000]; op.processed_at=now()
        results.append({'client_operation_id':op.client_operation_id,'status':op.status,'result':op.result,'error':op.error})
    for resource,cursor in batch.cursors.items():
        row=db.scalar(select(MobileSyncCursor).where(MobileSyncCursor.user_id==u.id,MobileSyncCursor.device_id==d.id,MobileSyncCursor.resource_type==resource))
        if not row: row=MobileSyncCursor(user_id=u.id,device_id=d.id,resource_type=resource)
        row.cursor_value=str(cursor)[:200]; row.updated_at=now(); db.add(row)
    db.commit()
    changed=db.scalars(select(Notification).where(Notification.user_id==u.id,Notification.created_at>now().replace(hour=0,minute=0,second=0,microsecond=0)).order_by(Notification.created_at.desc()).limit(100)).all()
    return {'results':results,'changes':{'notifications':[serialize_notification(x) for x in changed]},'server_time':now()}
