"""Cards: one JSON per asset version at cards/<kind>/<group>/<class>/<slug>/v<N>.json.

The RELEASE hash is what a game pins: the fields a game reads at run time + the file hashes + the dependency graph.
Names, tags, summaries, status and quality can be edited without a new release; anything a game uses cannot.
"""
import glob, hashlib, json, os, tempfile

RUNTIME = ('id', 'version', 'frame', 'size', 'bounds', 'placement', 'sockets', 'nodes', 'paint', 'rig', 'animations',
           'gameplay', 'files', 'deps')
REQUIRED = ('id', 'version', 'name', 'summary', 'style', 'status', 'frame', 'size', 'bounds', 'render', 'provenance',
            'licence', 'quality', 'files', 'release')
STATUS = ('draft', 'candidate', 'approved', 'legacy', 'deprecated')
# provenance.parent {id, version, release} + provenance.variant {kind, note}: lineage of a custom version. Not part of the
# release hash (provenance is not in RUNTIME), so it can be backfilled in place on existing card files.
VARIANT_KINDS = ('retexture', 'recolor', 'shape', 'resize', 'rig', 'rerun', 'alternative', 'other')
# a change to any of these is BREAKING (the Factory asks before upgrading a game)
BREAKING = ('nodes', 'sockets', 'frame', 'size', 'paint', 'rig')


def canonical(o):
    return json.dumps(o, sort_keys=True, separators=(',', ':'), ensure_ascii=False)


def release_hash(card):
    return 'sha256:' + hashlib.sha256(canonical({k: card.get(k) for k in RUNTIME}).encode()).hexdigest()


def path_of(root, card):
    cls, slug = card['id'].split('/', 1)
    return os.path.join(root, 'cards', *cls.split('.'), slug, f"v{card['version']}.json")


def validate(card):
    errs = [f'missing {k}' for k in REQUIRED if k not in card]
    cls = card.get('id', '').split('/')[0].split('.')
    if len(cls) != 3:
        errs.append(f'id must be kind.group.class/slug: {card.get("id")}')
    if card.get('status') not in STATUS:
        errs.append(f'bad status {card.get("status")}')
    if card.get('release') and card['release'] != release_hash(card):
        errs.append('release hash does not match the runtime fields')
    lic = card.get('licence') or {}
    if not lic.get('evidence'):
        errs.append('licence without evidence')
    if card.get('status') == 'approved' and not lic.get('game_use'):
        errs.append('approved but not cleared for games')
    errs += lineage_errors(card)
    return errs


def lineage_errors(card):
    pv = card.get('provenance') or {}
    errs = []
    par, var = pv.get('parent'), pv.get('variant')
    if par is None and var is None:
        return errs
    if not isinstance(par, dict) or not all(par.get(k) for k in ('id', 'version', 'release')):
        errs.append('provenance.parent needs id, version, release')
    elif par['id'] == card.get('id') and par['version'] >= card.get('version', 0):
        errs.append('provenance.parent must be an earlier version when it has the same id')
    if not isinstance(var, dict) or var.get('kind') not in VARIANT_KINDS:
        errs.append(f'provenance.variant.kind must be one of {VARIANT_KINDS}')
    return errs


def write(root, card):
    errs = validate(card)
    if errs:
        raise ValueError(f"{card.get('id')}: {errs}")
    p = path_of(root, card)
    os.makedirs(os.path.dirname(p), exist_ok=True)
    with tempfile.NamedTemporaryFile(mode='w', dir=os.path.dirname(p), suffix='.tmp', delete=False) as f:
        f.write(json.dumps(card, indent=1, ensure_ascii=False) + '\n')
        tmp = f.name
    os.replace(tmp, p)
    return p


def load_all(root):
    """{id: [card v1, v2, ...]} sorted by version."""
    out = {}
    for p in glob.glob(os.path.join(root, 'cards', '**', 'v*.json'), recursive=True):
        c = json.load(open(p))
        out.setdefault(c['id'], []).append(c)
    for v in out.values():
        v.sort(key=lambda c: c['version'])
    return out


def active(versions):
    """The version games get (the index row): the one marked `active`, else the newest that is not a draft or withdrawn.
    A lighter copy is written as a draft, so it never reaches a game until someone presses 'Use in games'."""
    marked = [c for c in versions if c.get('active')]
    if marked:
        return marked[-1]
    real = [c for c in versions if c.get('status') not in ('draft', 'deprecated')]
    return (real or versions)[-1]


def breaking_change(old, new):
    return [k for k in BREAKING if canonical(old.get(k)) != canonical(new.get(k))]
