"""Preseason ownership/pricing reference and exact dual-copy record format.

The native implementation must agree with this independent state oracle.
Native G/F/D eligibility is used for transfers; C/LW/RW are preferred line roles,
with ordinary forwards allowed to fill another forward role as in the editor.
"""
from dataclasses import dataclass,replace
import binascii,hashlib,json,struct

BUDGET=99**2+79**2
CAPACITY=26
SIZE=2048
MAP=32
FUNDS=1696
COUNTS=1824
COMMIT=ord('T')
ORDER={'G':0,'LW':1,'C':2,'RW':3,'F':4,'D':5}
ROLES=tuple(ORDER)

class TransferError(ValueError):pass

@dataclass(frozen=True)
class State:
 rosters:tuple
 budgets:tuple
 enabled:bool=True
 ready:bool=False
 sequence:int=1

class Market:
 def __init__(self,players):
  self.players={p['uid']:p for p in players};assert set(self.players)==set(range(832))
  canonical={}
  for p in players:
   assert p['position']in ORDER and 0<=p['overall']<=99
   old=canonical.get(p['nhl_id'])
   if old is None or(p['position']!='F',p['overall'],p['raw_overall'])>(old['position']!='F',old['overall'],old['raw_overall']):canonical[p['nhl_id']]=p
  self.allowed=frozenset(p['uid']for p in canonical.values());assert len(self.allowed)==831
  self.aliases={p['uid']:canonical[p['nhl_id']]['uid']for p in players if p['uid']not in self.allowed}
  self.initial_currency=32*BUDGET+sum(self.price(uid)for uid in self.allowed)
  identities=[(p['uid'],p['nhl_id'],p['position'],p['overall'],p['raw_overall'])for p in players]
  self.catalogue_crc=binascii.crc32(json.dumps(identities,separators=(',',':')).encode())&0xffffffff
 def price(self,uid):return self.players[uid]['overall']**2
 def sort(self,row):return tuple(sorted(row,key=lambda u:(ORDER[self.players[u]['position']],-self.players[u]['overall'],-self.players[u]['raw_overall'],self.players[u]['nhl_id'])))
 def counts(self,row):return tuple(sum(self.players[u]['position']==role for u in row)for role in ROLES)
 def roster_valid(self,row):
  if not 20<=len(row)<=CAPACITY:return False
  g,l,c,r,f,d=self.counts(row)
  return 2<=g<=3 and d>=6 and l+c+r+f>=12
 def initial(self,enabled=True):
  s=State(tuple(self.sort([u for u in self.allowed if self.players[u]['team']==club])for club in range(32)),(BUDGET,)*32,enabled)
  self.validate(s);return s
 def validate(self,s):
  if len(s.rosters)!=32 or len(s.budgets)!=32:raise TransferError('record dimensions')
  if type(s.enabled)is not bool or type(s.ready)is not bool or not 0<=s.sequence<=0xffffffff:raise TransferError('record flags/sequence')
  if s.ready and not s.enabled:raise TransferError('disabled ready flag')
  seen=set()
  for row in s.rosters:
   if not set(row)<=self.allowed:raise TransferError('unknown/duplicate-identity alias')
   if seen.intersection(row)or len(set(row))!=len(row):raise TransferError('duplicate ownership')
   seen.update(row)
   if row!=self.sort(row)or not self.roster_valid(row):raise TransferError('invalid roster/positions')
  if any(type(v)is not int or not 0<=v<=self.initial_currency for v in s.budgets):raise TransferError('invalid budget')
  if sum(s.budgets)+sum(self.price(u)for u in seen)!=self.initial_currency:raise TransferError('currency invariant')
  return s
 def owner(self,s,uid):
  if uid not in self.allowed:raise TransferError('unavailable identity')
  return next((club for club,row in enumerate(s.rosters)if uid in row),None)
 def free_agents(self,s):return self.allowed-frozenset(u for row in s.rosters for u in row)
 def editable(self,s,games_played):
  self.validate(s)
  if games_played!=0:raise TransferError('season locked')
  if not s.enabled:raise TransferError('transfers disabled')
 def changed(self,s,rosters,budgets,**flags):return self.validate(replace(s,rosters=tuple(rosters),budgets=tuple(budgets),sequence=(s.sequence+1)&0xffffffff,ready=False,**flags))
 def buy(self,s,buyer,uid,games_played=0):
  self.editable(s,games_played)
  if not 0<=buyer<32:raise TransferError('invalid club')
  seller=self.owner(s,uid);price=self.price(uid)
  if seller==buyer:raise TransferError('already owned')
  if len(s.rosters[buyer])>=CAPACITY:raise TransferError('roster full; sell first')
  if s.budgets[buyer]<price:raise TransferError('insufficient funds')
  rows=list(s.rosters);funds=list(s.budgets)
  if seller is not None:
   rows[seller]=self.sort([u for u in rows[seller]if u!=uid])
   if not self.roster_valid(rows[seller]):raise TransferError('seller needs position/players')
   funds[seller]+=price
  rows[buyer]=self.sort(rows[buyer]+(uid,));funds[buyer]-=price
  return self.changed(s,rows,funds)
 def sell(self,s,club,uid,games_played=0):
  self.editable(s,games_played)
  if self.owner(s,uid)!=club:raise TransferError('not owned')
  rows=list(s.rosters);funds=list(s.budgets);rows[club]=self.sort([u for u in rows[club]if u!=uid])
  if not self.roster_valid(rows[club]):raise TransferError('club needs position/players')
  funds[club]+=self.price(uid);return self.changed(s,rows,funds)
 def set_enabled(self,s,value,games_played=0):
  self.validate(s)
  if games_played!=0:raise TransferError('season locked')
  if type(value)is not bool:raise TransferError('invalid option')
  if value==s.enabled:return s
  return self.validate(replace(s,enabled=value,ready=False,sequence=(s.sequence+1)&0xffffffff))
 def mark_ready(self,s,games_played=0):
  self.editable(s,games_played);return s if s.ready else self.validate(replace(s,ready=True,sequence=(s.sequence+1)&0xffffffff))
 @staticmethod
 def crc(b):return binascii.crc_hqx(bytes(b[1:16])+bytes(b[20:]),0xffff)
 def encode(self,s,league,committed=True):
  self.validate(s);assert league in [1,2,3];b=bytearray(SIZE);b[0:4]=b'TRNF';b[4:8]=bytes([1,league,int(s.enabled),int(s.ready)]);struct.pack_into('>II',b,8,s.sequence,s.sequence^0xffffffff)
  # Fixed explicit fields: CRC16/+inverse16, currency32, identities16/schema16, catalogueCRC32.
  struct.pack_into('>IHHI',b,20,self.initial_currency,len(self.allowed),2,self.catalogue_crc)
  for club,row in enumerate(s.rosters):
   struct.pack_into('>26H',b,MAP+52*club,*(row+(0xffff,)*(26-len(row))));struct.pack_into('>I',b,FUNDS+4*club,s.budgets[club]);b[COUNTS+6*club:COUNTS+6*club+6]=bytes(self.counts(row))
  c=self.crc(b);struct.pack_into('>HH',b,16,c,c^65535)
  if not committed:b[0]=0
  return bytes(b)
 def decode(self,b,league):
  if len(b)!=SIZE or b[:4]!=b'TRNF'or b[4]!=1 or b[5]!=league or b[6]>1 or b[7]>1:raise TransferError('record tag/version/context')
  seq,inv=struct.unpack_from('>II',b,8);crc,cinv=struct.unpack_from('>HH',b,16)
  if seq^inv!=0xffffffff or crc^cinv!=65535 or crc!=self.crc(b):raise TransferError('record checksum/sequence')
  currency,count,schema,cat=struct.unpack_from('>IHHI',b,20)
  if(currency,count,schema,cat)!=(self.initial_currency,831,2,self.catalogue_crc):raise TransferError('record catalogue/model')
  rows=[]
  for club in range(32):
   slots=struct.unpack_from('>26H',b,MAP+52*club);n=next((i for i,u in enumerate(slots)if u==65535),26)
   if slots[n:]!=(65535,)*(26-n):raise TransferError('roster holes')
   row=slots[:n]
   if not set(row)<=self.allowed:raise TransferError('invalid UID')
   if tuple(b[COUNTS+6*club:COUNTS+6*club+6])!=self.counts(row):raise TransferError('roster counts')
   rows.append(row)
  if any(b[2016:]):raise TransferError('reserved record bytes')
  return self.validate(State(tuple(rows),struct.unpack_from('>32I',b,FUNDS),bool(b[6]),bool(b[7]),seq))
 def select(self,records,league):
  valid=[]
  for i,b in enumerate(records):
   try:s=self.decode(b,league)
   except TransferError:continue
   valid.append((i,s))
  if not valid:return None
  if len(valid)==1:return valid[0]
  a,b=valid;diff=(b[1].sequence-a[1].sequence)&0xffffffff
  if diff==0:
   if a[1]!=b[1]:raise TransferError('ambiguous equal-sequence records')
   return a
  if diff==0x80000000:raise TransferError('ambiguous sequence distance')
  return b if diff<0x80000000 else a
