"""Connection-owned, bounded pages. Paths never cross the device protocol."""
import asyncio
import json
import time
from collections import OrderedDict
from uuid import uuid4
from .models import ControlError

class Catalog:
    def __init__(self, projects, adapters, ttl=60, clock=time.monotonic):
        self.projects,self.adapters,self.ttl,self.clock=projects,adapters,ttl,clock
        self.snapshots=OrderedDict()
    async def page(self, connection, kind, project_id=None, snapshot=None, cursor=0):
        if type(cursor) is not int or cursor<0: raise ControlError('Stale cursor',409)
        if snapshot:
            data=self.snapshots.get(snapshot)
            if not data or data['connection']!=connection or data['kind']!=kind or data['project_id']!=project_id: raise ControlError('Stale snapshot',409)
            if self.clock()>=data['expires']: raise ControlError('Expired snapshot',410)
        else:
            if cursor: raise ControlError('Stale cursor',409)
            errors={};items=[]
            if kind=='projects':
                items=[dict(project_id=p.id,name=p.name.encode()[:240].decode(errors="ignore")) for p in self.projects.list()]
            elif kind=='sessions':
                project=self.projects.get(project_id)
                results=await asyncio.gather(*(asyncio.wait_for(a.list_sessions(project),15) for a in self.adapters.values()),return_exceptions=True)
                for agent,result in zip(self.adapters,results):
                    if isinstance(result,Exception): errors[agent]='Adapter unavailable'
                    else:
                        for s in result:
                            if s.project_id==project.id and s.agent==agent:
                                items.append(dict(project_id=project.id,agent=agent,session_id=s.session_id,name=s.name.encode()[:240].decode(errors="ignore"),updated_at=s.updated_at,state=s.state))
                items.sort(key=lambda s:(s['updated_at'],s['agent'],s['session_id']),reverse=True)
            else: raise ControlError('Unsupported catalog',400)
            if len(items)>10000: raise ControlError('Catalog too large',429)
            snapshot=uuid4().hex
            data=dict(connection=connection,kind=kind,project_id=project_id,items=items,errors=errors,expires=self.clock()+self.ttl)
            self.snapshots[snapshot]=data
            while len(self.snapshots)>64: self.snapshots.popitem(last=False)
        if cursor>len(data['items']) or cursor%4: raise ControlError('Stale cursor',409)
        items=data['items'][cursor:cursor+4];next_cursor=cursor+len(items)
        status='Partial' if data['errors'] and items else 'Adapter unavailable' if data['errors'] else 'Empty' if not data['items'] else 'Ready'
        result=dict(type='catalog_page',kind=kind,project_id=project_id,snapshot=snapshot,cursor=cursor,next_cursor=next_cursor if next_cursor<len(data['items']) else None,ttl_seconds=max(0,int(data['expires']-self.clock())),status=status,errors=data['errors'],items=items)
        if len(json.dumps(result,ensure_ascii=False).encode())>3900: raise ControlError('Catalog frame too large',413)
        return result
