from __future__ import annotations
import json
import os
from pathlib import Path

class TargetStore:
    """Durable selection and monotonic generation, including legacy migration."""
    def __init__(self, path): self.path = Path(path).expanduser()
    def _load(self):
        from .control.models import ControlError
        try: raw = self.path.read_text()
        except FileNotFoundError: return {}
        except OSError as exc: raise ControlError('Target store unavailable') from exc
        if not raw.strip(): return {}  # newly provisioned empty state file
        try: value = json.loads(raw)
        except ValueError as exc: raise ControlError('Invalid target store; restore before selecting') from exc
        if not isinstance(value, dict): raise ControlError('Invalid target store')
        result = {}
        for key, item in value.items():
            if isinstance(item, str): item = {'target':normalize_target(item), 'generation':0, 'project_id':None}
            if (isinstance(item, dict) and isinstance(item.get('target'),str)
                    and type(item.get('generation')) is int and 0 <= item['generation'] < 2**53): result[key]=item
            else: raise ControlError('Invalid target store generation')
        return result
    def selection(self, device_id):
        return self._load().get(device_id, {'target':'server', 'generation':0, 'project_id':None}).copy()
    def get(self, device_id): return normalize_target(self.selection(device_id)['target'])
    def generation(self, device_id): return self.selection(device_id)['generation']
    def set(self, device_id, target, project_id=None, *, expected=None):
        targets=self._load(); previous=self.selection(device_id)
        if expected is not None and (type(expected) is not int or previous['generation'] != expected): return None
        if previous['generation']>=2**53-1:raise RuntimeError('Target generation exhausted')
        value=dict(target=normalize_target(target),generation=previous['generation']+1,project_id=project_id)
        targets[device_id]=value
        self.path.parent.mkdir(parents=True,exist_ok=True,mode=0o700)
        temporary=self.path.with_name('.'+self.path.name+'.tmp')
        with temporary.open('w') as stream:
            os.chmod(temporary,0o600);json.dump(targets,stream,ensure_ascii=False);stream.flush();os.fsync(stream.fileno())
        temporary.replace(self.path)
        fd=os.open(self.path.parent,os.O_RDONLY)
        try: os.fsync(fd)
        finally: os.close(fd)
        return value.copy()

def normalize_target(target): return 'agent:'+target if target.startswith('pi:') else target

def split_target(target):
    parts=normalize_target(target).split(':',2)
    return (parts[1],parts[2]) if len(parts)==3 and parts[0]=='agent' and parts[1] in ('pi','codex') and parts[2] else None
