"""Pure map identity, UV/map transform and durable queue contract."""
from copy import deepcopy
from datetime import datetime, timezone
from pathlib import Path
import hashlib
import json
import math

class MapRequestError(ValueError): pass

def require(test, message):
    if not test: raise MapRequestError(message)

def pair(value):
    require(isinstance(value,(list,tuple)) and len(value)==2, '좌표 두 값이 필요합니다.')
    require(all(type(v) in (float,int) and math.isfinite(v) for v in value), '유효한 좌표가 아닙니다.')
    return [float(v) for v in value]

def uv_to_map(uv, size):
    u,v=pair(uv)
    require(0<=u<=1 and 0<=v<=1 and type(size) in (float,int) and math.isfinite(size) and size>0,'지도 범위 밖입니다.')
    return [(u-.5)*size,(.5-v)*size]

def current_owner(snapshot):
    if not snapshot: return None
    result=[snapshot.get('pid'),snapshot.get('process_started_at'),(snapshot.get('identity') or {}).get('character_id')]
    require(type(result[0]) is int and result[0]>0 and all(isinstance(v,str) and v for v in result[1:]),'현재 연결 근거가 없습니다.')
    return result

def fresh(snapshot, now=None):
    try:
        observed=datetime.fromisoformat(snapshot['captured_at'].replace('Z','+00:00'))
        age=((now or datetime.now(timezone.utc))-observed).total_seconds()
        return observed.tzinfo is not None and -.5<=age<=5
    except (TypeError,KeyError,ValueError): return False

class Atlas:
    def __init__(self, root=None):
        self.root=Path(root or Path(__file__).resolve().parents[1])
        self.bundle=json.loads((self.root/'data/bundle.json').read_text(encoding='utf-8'))
        require(self.bundle.get('schema')==1,'지도 묶음 스키마를 지원하지 않습니다.')
        require(set(self.bundle['files'])=={'assets/manifest.json','data/coordinates.json'},'지도 묶음 파일 구성이 다릅니다.')
        for name,digest in self.bundle['files'].items():
            require(hashlib.sha256((self.root/name).read_bytes()).hexdigest()==digest,'지도 묶음 파일 무결성 오류: '+name)
        self.manifest=json.loads((self.root/'assets/manifest.json').read_text(encoding='utf-8'))
        coordinates=json.loads((self.root/'data/coordinates.json').read_text(encoding='utf-8'))
        require(self.manifest.get('schema')==coordinates.get('schema')==1,'지도 좌표 스키마 오류')
        self.manifest['game_spaces']=coordinates['game_spaces']
        links={r['resource']:r for r in coordinates['maps']}
        self.manifest['maps']=[dict(r,**{k:v for k,v in links[r['resource']].items() if k!='resource'}) for r in self.manifest['maps']]
        self.maps={r['resource']:r for r in self.manifest['maps']}
        self.markers={r['key']:r for r in self.manifest.get('markers',[])}

    def marker(self,key):
        require(isinstance(key,str) and key in self.markers,'선택한 NPC/던전의 배치 근거가 없습니다.')
        return self.markers[key]

    def version_status(self, observed=None):
        mismatched=bool(observed and any(observed.get(k)!=v for k,v in self.bundle['source_version'].items()))
        return dict(bundle_id=self.bundle['bundle_id'],source_version=self.bundle['source_version'],
                    version_mismatch=mismatched,blocked=False,
                    note='게임 버전이 다릅니다. 기존 자료로 계속 운용하며 현재 지도와 좌표를 확인합니다.' if mismatched else '기존 지도 자료로 운용합니다.')

    def resource(self, name):
        require(isinstance(name,str) and name in self.maps,'해당 지도 이미지가 없습니다. 리소스에 맞는 지도 타일이 필요합니다.')
        return self.maps[name]

    def validate(self, payload):
        require(isinstance(payload,dict) and set(payload)=={'kind','map_module','bundle_id','method','destination','action'},'지도 큐 형식 오류')
        require(payload['bundle_id']==self.bundle['bundle_id'],'해당 목적지를 선택한 지도 묶음을 보존 위치에서 불러와 주세요.')
        require(payload['kind']=='map_move' and payload['map_module']==1 and payload['method'] in ('coordinate','spot')
                and payload['action']=='walk','일반 지도 이동 요청만 허용합니다.')
        d=payload['destination']
        keys={'resource','world_data_key','uv','image_sha256','expected_owner'}
        if payload['method']=='spot':keys.add('marker_key')
        require(isinstance(d,dict) and set(d)==keys,'목적지 형식 오류')
        asset=self.resource(d['resource'])
        require(asset['map_type']!=1 and asset['world_data_key']==d['world_data_key'],'월드맵에서는 지역을 먼저 선택해 주세요.')
        require(d['image_sha256']==asset['image_sha256'],'선택 이후 지도 이미지가 변경됐습니다.')
        path=(self.root/asset['image']).resolve()
        require(path.is_relative_to((self.root/'assets').resolve()),'지도 자산 경로 오류')
        require(hashlib.sha256(path.read_bytes()).hexdigest()==asset['image_sha256'],'지도 이미지 해시가 일치하지 않습니다.')
        uv_to_map(d['uv'],asset['world_size'])
        if payload['method']=='spot':
            marker=self.marker(d['marker_key'])
            require(marker['resource']==d['resource'] and marker['world_data_key']==d['world_data_key'] and
                    marker['uv']==d['uv'],'NPC/던전 배치와 선택 좌표가 다릅니다.')
        owner=d['expected_owner']
        require(owner is None or isinstance(owner,list) and len(owner)==3 and type(owner[0]) is int and owner[0]>0
                and all(isinstance(x,str) and x for x in owner[1:]),'현재 연결 정보 오류')
        return deepcopy(payload)

    def select(self, resource, uv, *, snapshot=None):
        asset=self.resource(resource)
        return self.validate(dict(kind='map_move',map_module=1,bundle_id=self.bundle['bundle_id'],method='coordinate',action='walk',
            destination=dict(resource=resource,world_data_key=asset['world_data_key'],uv=pair(uv),
                             image_sha256=asset['image_sha256'],expected_owner=current_owner(snapshot) if fresh(snapshot) else None)))

    def request(self, payload):
        p=self.validate(payload);d=p['destination'];asset=self.resource(d['resource'])
        if p['method']=='spot':
            marker=self.marker(d['marker_key'])
            goal=dict(world_data_key=marker['world_data_key'],title=marker['name'],world_position=marker['world_position'])
            for key in ('npc_id','marker_id'):
                if marker.get(key) is not None:goal[key]=marker[key]
            return dict(method='spot',goal=goal)
        return dict(method='coordinate',world_data_key=d['world_data_key'],map_position=uv_to_map(d['uv'],asset['world_size']))

    def verify_current(self, payload, snapshot):
        d=payload['destination'];asset=self.resource(d['resource']);pose=snapshot.get('pose') or {}
        require(snapshot.get('map_ready') and not snapshot.get('map_transition') and fresh(snapshot),'현재 지도 배치가 준비되지 않았습니다.')
        require(snapshot.get('world_data_key')==d['world_data_key'] and pose.get('resource')==d['resource']
                and pose.get('map_type')!=1 and pose.get('world_size')==asset['world_size'],'지역·리소스·지도 크기가 선택한 이미지와 다릅니다.')
        require(d['expected_owner'] is None or current_owner(snapshot)==d['expected_owner'],'선택 이후 캐릭터/게임 연결이 바뀌었습니다.')
        return uv_to_map(d['uv'],pose['world_size'])
