#!/usr/bin/env python3
"""
Unified Dart version table generator for r2flutter.

Reads/writes offsets.json (the source of truth for hashes/profiles) and
generates dart_offsets.h, dart_version.c, and batch-refreshed dart_cid.c tables.

Usage:
  # Add a new Dart version (pulls SDK, computes hash)
  ./scripts/update-dart-version 3.13.0

  # Add from a local SDK checkout
  ./scripts/update-dart-version --sdk-dir /path/to/dart-sdk 3.13.0

  # Add a hash manually (no SDK pull)
  ./scripts/update-dart-version --hash <md5> 3.13.0

  # Regenerate C files from offsets.json
  ./scripts/update-dart-version --generate

  # Pull stable Dart SDK tags and rebuild version/hash/CID tables
  ./scripts/update-dart-version --sync-stable-tags --min-version 2.10.0

  # Show current known versions
  ./scripts/update-dart-version --list
"""

import argparse
import hashlib
import json
import os
import re
import subprocess
import sys
import tempfile

# ── Files that contribute to the snapshot hash ──────────────────────────
VM_SNAPSHOT_FILES = [
    'app_snapshot.cc', 'app_snapshot.h',
    'dart.cc', 'dart_api_impl.cc',
    'datastream.h',
    'image_snapshot.cc', 'image_snapshot.h',
    'object.cc', 'object.h',
    'raw_object.cc', 'raw_object.h',
    'snapshot.cc', 'snapshot.h',
    'symbols.cc', 'symbols.h',
]

TAG_STYLE_NAMES = {
    "CID_INT32": "DART_TAG_STYLE_CID_INT32",
    "CID_SHIFT1": "DART_TAG_STYLE_CID_SHIFT1",
    "OBJECT_HEADER": "DART_TAG_STYLE_OBJECT_HEADER",
}

HEADER_STYLE_NAMES = {
    "modern": "DART_HEADER_STYLE_MODERN",
    "2.10": "DART_HEADER_STYLE_210",
    "2.12": "DART_HEADER_STYLE_212",
    "2.14": "DART_HEADER_STYLE_214",
    "2.16": "DART_HEADER_STYLE_216",
}

REF_ENCODING_NAMES = {
    "ref_id128": "DART_REF_ENCODING_REF_ID128",
    "unsigned": "DART_REF_ENCODING_UNSIGNED",
}

POOL_ENCODING_NAMES = {
    "modern": "DART_POOL_ENCODING_MODERN",
    "low7": "DART_POOL_ENCODING_LOW7",
    "low7_swapped": "DART_POOL_ENCODING_LOW7_SWAPPED",
}

CODE_ALLOC_NAMES = {
    "state_deferred": "DART_CODE_ALLOC_STATE_DEFERRED",
    "deferred_only": "DART_CODE_ALLOC_DEFERRED_ONLY",
}

TAG_STYLE_VALUES = {v: k for k, v in TAG_STYLE_NAMES.items()}

CID_KIND_TO_CLASS_NAME = [
    ("ILLEGAL", "Illegal"),
    ("NATIVE_POINTER", "NativePointer"),
    ("CLASS", "Class"),
    ("PATCH_CLASS", "PatchClass"),
    ("FUNCTION", "Function"),
    ("TYPE_PARAMETERS", "TypeParameters"),
    ("CLOSURE_DATA", "ClosureData"),
    ("FFI_TRAMPOLINE_DATA", "FfiTrampolineData"),
    ("FIELD", "Field"),
    ("SCRIPT", "Script"),
    ("LIBRARY", "Library"),
    ("NAMESPACE", "Namespace"),
    ("KERNEL_PROGRAM_INFO", "KernelProgramInfo"),
    ("WEAK_SERIALIZATION_REFERENCE", "WeakSerializationReference"),
    ("WEAK_ARRAY", "WeakArray"),
    ("CODE", "Code"),
    ("OBJECT_POOL", "ObjectPool"),
    ("PC_DESCRIPTORS", "PcDescriptors"),
    ("CODE_SOURCE_MAP", "CodeSourceMap"),
    ("COMPRESSED_STACK_MAPS", "CompressedStackMaps"),
    ("LOCAL_VAR_DESCRIPTORS", "LocalVarDescriptors"),
    ("EXCEPTION_HANDLERS", "ExceptionHandlers"),
    ("CONTEXT", "Context"),
    ("CONTEXT_SCOPE", "ContextScope"),
    ("SENTINEL", "Sentinel"),
    ("SINGLE_TARGET_CACHE", "SingleTargetCache"),
    ("UNLINKED_CALL", "UnlinkedCall"),
    ("MONOMORPHIC_SMIABLE_CALL", "MonomorphicSmiableCall"),
    ("CALL_SITE_DATA", "CallSiteData"),
    ("ICDATA", "ICData"),
    ("MEGAMORPHIC_CACHE", "MegamorphicCache"),
    ("SUBTYPE_TEST_CACHE", "SubtypeTestCache"),
    ("LOADING_UNIT", "LoadingUnit"),
    ("API_ERROR", "ApiError"),
    ("LANGUAGE_ERROR", "LanguageError"),
    ("UNHANDLED_EXCEPTION", "UnhandledException"),
    ("UNWIND_ERROR", "UnwindError"),
    ("INSTANCE", "Instance"),
    ("LIBRARY_PREFIX", "LibraryPrefix"),
    ("TYPE_ARGUMENTS", "TypeArguments"),
    ("TYPE", "Type"),
    ("FUNCTION_TYPE", "FunctionType"),
    ("RECORD_TYPE", "RecordType"),
    ("TYPE_REF", "TypeRef"),
    ("TYPE_PARAMETER", "TypeParameter"),
    ("CLOSURE", "Closure"),
    ("MINT", "Mint"),
    ("DOUBLE", "Double"),
    ("FLOAT32X4", "Float32x4"),
    ("INT32X4", "Int32x4"),
    ("FLOAT64X2", "Float64x2"),
    ("RECORD", "Record"),
    ("TYPED_DATA", "TypedData"),
    ("EXTERNAL_TYPED_DATA", "ExternalTypedData"),
    ("TYPED_DATA_VIEW", "TypedDataView"),
    ("CAPABILITY", "Capability"),
    ("RECEIVE_PORT", "ReceivePort"),
    ("SEND_PORT", "SendPort"),
    ("STACK_TRACE", "StackTrace"),
    ("SUSPEND_STATE", "SuspendState"),
    ("REGEXP", "RegExp"),
    ("WEAK_PROPERTY", "WeakProperty"),
    ("WEAK_REFERENCE", "WeakReference"),
    ("FUTURE_OR", "FutureOr"),
    ("USER_TAG", "UserTag"),
    ("TRANSFERABLE_TYPED_DATA", "TransferableTypedData"),
    ("MAP", "Map"),
    ("CONST_MAP", "ConstMap"),
    ("SET", "Set"),
    ("CONST_SET", "ConstSet"),
    ("ARRAY", "Array"),
    ("IMMUTABLE_ARRAY", "ImmutableArray"),
    ("GROWABLE_OBJECT_ARRAY", "GrowableObjectArray"),
    ("STRING", "String"),
    ("ONE_BYTE_STRING", "OneByteString"),
    ("TWO_BYTE_STRING", "TwoByteString"),
    ("NUM_PREDEFINED_CIDS", "NumPredefinedCids"),
]

STABLE_TAG_RE = re.compile(r"^\d+\.\d+\.\d+$")

# Some Flutter engine snapshots map to a Dart SDK version string but use a CID
# layout that does not match the standalone dart-lang/sdk tag for that version.
CID_TABLE_VERSION_ALIASES = {
    "2.18.2": "3.9.2",
}

LAYOUT_KEYS = [
    "cws",
    "heap_object_tag",
    "max_alignment",
    "it_cap",
    "tag_style",
    "header_fields",
]

CID_META_KEYS = [
    "typed_data_internal_base",
    "typed_data_internal_limit",
    "typed_data_stride",
]


# ═══════════════════════════════════════════════════════════════════════
# JSON I/O
# ═══════════════════════════════════════════════════════════════════════

def load_offsets(filepath):
    with open(filepath) as f:
        return json.load(f)


def compact_json(value):
    return json.dumps(value, separators=(", ", ": "))


def format_offsets(data):
    defaults = data.get("defaults", {})
    profiles = data.get("profiles", {})
    hashes = data.get("hashes", {})
    lines = [
        "{",
        '  "defaults": {',
        f'    "code_entry_point_offsets": {compact_json(defaults.get("code_entry_point_offsets", [8, 24, 16, 32]))},',
        f'    "code_owner_offsets": {compact_json(defaults.get("code_owner_offsets", [56]))},',
        f'    "function_name_offsets": {compact_json(defaults.get("function_name_offsets", [24, 8]))}',
        "  },",
        '  "profiles": {',
    ]
    profile_items = sorted(profiles.items(), key=lambda kv: parse_version(kv[0]))
    for i, (version, profile) in enumerate(profile_items):
        comma = "," if i + 1 < len(profile_items) else ""
        lines.append(f'    "{version}": {compact_json(profile)}{comma}')
    lines.extend([
        "  },",
        '  "hashes": {',
    ])
    hash_items = sorted(hashes.items(), key=lambda kv: (parse_version(kv[1]), kv[0]))
    for i, (snapshot_hash, version) in enumerate(hash_items):
        comma = "," if i + 1 < len(hash_items) else ""
        lines.append(f'    "{snapshot_hash}": "{version}"{comma}')
    lines.extend([
        "  }",
        "}",
        "",
    ])
    return "\n".join(lines)


def save_offsets(filepath, data):
    with open(filepath, 'w') as f:
        f.write(format_offsets(data))
    print(f"  Updated {filepath}")


# ═══════════════════════════════════════════════════════════════════════
# Helpers
# ═══════════════════════════════════════════════════════════════════════

def parse_version(ver_str):
    return tuple(int(p) for p in ver_str.split("."))


def compute_snapshot_hash(sdk_vm_dir):
    """Compute the snapshot hash the same way Dart's make_version.py does."""
    h = hashlib.md5()
    for fname in VM_SNAPSHOT_FILES:
        fpath = os.path.join(sdk_vm_dir, fname)
        if os.path.exists(fpath):
            with open(fpath, 'rb') as f:
                h.update(f.read())
        else:
            print(f"  Warning: {fname} not found in {sdk_vm_dir}", file=sys.stderr)
    return h.hexdigest()


def strip_comments(content):
    content = re.sub(r'/\*.*?\*/', '', content, flags=re.DOTALL)
    content = re.sub(r'//.*', '', content)
    return content


def parse_macro_lists(content):
    """Return class-id macro lists such as CLASS_LIST(V) as ordered names."""
    clean = strip_comments(content)
    raw_macros = {}
    lines = clean.splitlines()
    i = 0
    while i < len(lines):
        line = lines[i]
        m = re.match(r'\s*#define\s+(\w+)\s*\(([^)]*)\)\s*(.*)', line)
        if not m:
            i += 1
            continue
        name = m.group(1)
        body = m.group(3)
        while body.rstrip().endswith('\\') and i + 1 < len(lines):
            body = body.rstrip()[:-1] + " " + lines[i + 1].strip()
            i += 1
        raw_macros[name] = body
        i += 1

    memo = {}

    item_re = re.compile(r'\bV\s*\(\s*(\w+)\s*\)|\b(\w*CLASS_LIST\w*|DART_CLASS_LIST\w*)\s*\(\s*V\s*\)')

    def expand(name):
        if name in memo:
            return memo[name]
        body = raw_macros.get(name, "")
        out = []
        for match in item_re.finditer(body):
            cls = match.group(1)
            nested = match.group(2)
            if cls:
                out.append(cls)
            elif nested and nested != name:
                out.extend(expand(nested))
        memo[name] = out
        return out

    return {name: expand(name) for name in raw_macros}


def cid_token_name(token):
    if not token.startswith("k"):
        return None
    name = token[1:]
    if name.endswith("Cid"):
        name = name[:-3]
    return name


def apply_define_object_kind(body, cls):
    text = body.replace("##", "")
    text = text.replace("clazz", cls)
    return re.findall(r'\bk[A-Za-z0-9_]+(?:Cid|Cids)?\b', text)


def parse_class_id_list_tokens(content, macro_lists):
    clean = strip_comments(content)
    m = re.search(r'#define\s+CLASS_ID_LIST\s+(.*?)(?:\n#undef\s+CLASS_ID_LIST|\nenum\s+ClassId)', clean, re.DOTALL)
    if not m:
        return []
    body = re.sub(r'\\\n\s*', ' ', m.group(1))
    tokens = []
    item_re = re.compile(
        r'\bCID\s*\(\s*(\w+)\s*\)|'
        r'\b(\w+)\s*\(\s*DEFINE_TYPED_DATA_CLASS_ID\s*\)|'
        r'\b(\w+)\s*\(\s*DEFINE_FFI_CLASS_ID\s*\)|'
        r'\b(\w+)\s*\(\s*DEFINE_CLASS_ID\s*\)')
    for match in item_re.finditer(body):
        direct, typed_list, ffi_list, class_list = match.groups()
        if direct:
            tokens.append(f"k{direct}")
            continue
        if typed_list:
            for cls in macro_lists.get(typed_list, []):
                tokens.extend([
                    f"kTypedData{cls}Cid",
                    f"kTypedData{cls}ViewCid",
                    f"kExternalTypedData{cls}Cid",
                    f"kUnmodifiableTypedData{cls}ViewCid",
                ])
            continue
        if ffi_list:
            for cls in macro_lists.get(ffi_list, []):
                tokens.append(f"kFfi{cls}Cid")
            continue
        if class_list:
            for cls in macro_lists.get(class_list, []):
                tokens.append(f"k{cls}Cid")
    return tokens


def parse_class_id_content(content):
    """Parse Dart runtime/vm/class_id.h and extract CID values."""
    macro_lists = parse_macro_lists(content)
    clean = strip_comments(content)
    enum_match = re.search(r'enum\s+ClassId(?:\s*:\s*intptr_t)?\s*\{(.*?)\};', clean, re.DOTALL)
    if not enum_match:
        raise ValueError("Could not find ClassId enum in class_id.h")
    enum_body = enum_match.group(1)
    cids = {}
    current_value = 0
    define_body = None
    logical_lines = []
    pending = ""
    for raw_line in enum_body.split('\n'):
        line = raw_line.rstrip()
        if line.endswith('\\'):
            pending += line[:-1] + " "
            continue
        if pending:
            line = pending + line.strip()
            pending = ""
        logical_lines.append(line)
    if pending:
        logical_lines.append(pending)

    for line in logical_lines:
        line = line.strip()
        if not line:
            continue

        define_match = re.match(r'#define\s+DEFINE_OBJECT_KIND\s*\([^)]*\)\s*(.*)', line)
        if define_match:
            define_body = define_match.group(1)
            continue
        if line.startswith("#undef DEFINE_OBJECT_KIND"):
            define_body = None
            continue

        list_match = re.match(r'(\w+)\s*\(\s*DEFINE_OBJECT_KIND\s*\)', line)
        if list_match and define_body:
            for cls in macro_lists.get(list_match.group(1), []):
                for token in apply_define_object_kind(define_body, cls):
                    name = cid_token_name(token)
                    if name:
                        cids[name] = current_value
                        current_value += 1
            continue

        if line == "CLASS_ID_LIST":
            for token in parse_class_id_list_tokens(content, macro_lists):
                name = cid_token_name(token)
                if name:
                    cids[name] = current_value
                    current_value += 1
            continue

        for part in [p.strip() for p in line.split(',') if p.strip()]:
            assign_match = re.match(r'(k\w+(?:Cid|Cids)?)\s*=\s*(\d+)$', part)
            if assign_match:
                name = cid_token_name(assign_match.group(1))
                cids[name] = int(assign_match.group(2))
                current_value = int(assign_match.group(2)) + 1
                continue
            implicit_match = re.match(r'(k\w+(?:Cid|Cids)?)$', part)
            if implicit_match:
                name = cid_token_name(implicit_match.group(1))
                cids[name] = current_value
                current_value += 1
    return cids


def parse_class_id_h(filepath):
    """Parse a Dart SDK class_id.h and extract CID values."""
    with open(filepath) as f:
        return parse_class_id_content(f.read())


def typed_data_info_from_cids(cids):
    base = cids.get("TypedDataInt8Array")
    next_base = cids.get("TypedDataUint8Array")
    if base is None:
        return 0, 0, 0
    stride = (next_base - base) if next_base is not None and next_base > base else 3
    last = cids.get("UnmodifiableTypedDataFloat64x2ArrayView")
    if last is None:
        last = cids.get("ExternalTypedDataFloat64x2Array")
    if last is None:
        last = base + (14 * stride) - 1
    return base, last + 1, stride


def pull_sdk_tag(sdk_repo, tag, work_dir):
    """Checkout a Dart SDK tag into work_dir and return the runtime/vm path."""
    result = subprocess.run(
        ["git", "clone", "--depth", "1", "--branch", tag, sdk_repo, work_dir],
        capture_output=True, text=True)
    if result.returncode != 0:
        result = subprocess.run(
            ["git", "clone", "--depth", "1", "--branch", f"tags/{tag}", sdk_repo, work_dir],
            capture_output=True, text=True)
    if result.returncode != 0:
        print(f"Error cloning {sdk_repo} tag {tag}: {result.stderr}", file=sys.stderr)
        return None
    vm_dir = os.path.join(work_dir, "runtime", "vm")
    if not os.path.isdir(vm_dir):
        print(f"Error: runtime/vm not found in cloned repo", file=sys.stderr)
        return None
    return vm_dir


def run_checked(cmd, **kwargs):
    result = subprocess.run(cmd, capture_output=True, text=True, **kwargs)
    if result.returncode != 0:
        raise RuntimeError(f"{' '.join(cmd)} failed: {result.stderr.strip()}")
    return result


def list_stable_sdk_tags(sdk_repo, min_version, max_version=None):
    result = run_checked(["git", "ls-remote", "--tags", sdk_repo, "refs/tags/[0-9]*"])
    tags = set()
    for line in result.stdout.splitlines():
        m = re.search(r"refs/tags/([^{}^]+)(?:\^\{\})?$", line)
        if not m:
            continue
        tag = m.group(1)
        if not STABLE_TAG_RE.fullmatch(tag):
            continue
        if parse_version(tag) < parse_version(min_version):
            continue
        if max_version and parse_version(tag) > parse_version(max_version):
            continue
        tags.add(tag)
    return sorted(tags, key=parse_version)


def ensure_partial_sdk_repo(sdk_repo, repo_dir):
    if os.path.isdir(os.path.join(repo_dir, ".git")):
        return
    os.makedirs(os.path.dirname(repo_dir), exist_ok=True)
    run_checked([
        "git", "clone", "--filter=blob:none", "--no-checkout", "--no-tags",
        sdk_repo, repo_dir
    ])


def fetch_sdk_tags(repo_dir, tags):
    chunk_size = 24
    for i in range(0, len(tags), chunk_size):
        chunk = tags[i:i + chunk_size]
        print(f"Fetching tags {chunk[0]}..{chunk[-1]}", flush=True)
        refspecs = [f"+refs/tags/{tag}:refs/tags/{tag}" for tag in chunk]
        run_checked(["git", "-C", repo_dir, "fetch", "--depth", "1", "origin"] + refspecs)


def git_show_text(repo_dir, tag, path):
    result = run_checked(["git", "-C", repo_dir, "show", f"{tag}:{path}"])
    return result.stdout


def git_show_bytes(repo_dir, tag, path):
    result = subprocess.run(
        ["git", "-C", repo_dir, "show", f"{tag}:{path}"],
        capture_output=True)
    if result.returncode != 0:
        raise RuntimeError(f"git show {tag}:{path} failed: {result.stderr.decode(errors='replace').strip()}")
    return result.stdout


def git_blob_bytes(repo_dir, tag, paths):
    result = run_checked(["git", "-C", repo_dir, "ls-tree", "-r", tag, "--"] + paths)
    oid_to_path = {}
    for line in result.stdout.splitlines():
        m = re.match(r'^\d+\s+blob\s+([0-9a-f]+)\t(.+)$', line)
        if m:
            oid_to_path[m.group(1)] = m.group(2)
    missing = [path for path in paths if path not in set(oid_to_path.values())]
    if not oid_to_path:
        return {}, missing

    oid_order = list(oid_to_path.keys())
    proc = subprocess.run(
        ["git", "-C", repo_dir, "cat-file", "--batch"],
        input=("".join(f"{oid}\n" for oid in oid_order)).encode(),
        capture_output=True)
    if proc.returncode != 0:
        raise RuntimeError(proc.stderr.decode(errors='replace').strip())

    blobs = {}
    pos = 0
    out = proc.stdout
    for oid in oid_order:
        nl = out.find(b'\n', pos)
        if nl < 0:
            raise RuntimeError(f"truncated cat-file output for {tag}:{oid_to_path[oid]}")
        header = out[pos:nl].decode(errors='replace')
        parts = header.split()
        if len(parts) < 3 or parts[1] != "blob":
            raise RuntimeError(f"unexpected cat-file header for {tag}:{oid_to_path[oid]}: {header}")
        size = int(parts[2])
        start = nl + 1
        end = start + size
        blobs[oid_to_path[oid]] = out[start:end]
        pos = end + 1
    return blobs, missing


def compute_snapshot_hash_from_git(repo_dir, tag):
    h = hashlib.md5()
    missing = []
    for fname in VM_SNAPSHOT_FILES:
        path = f"runtime/vm/{fname}"
        try:
            h.update(git_show_bytes(repo_dir, tag, path))
        except RuntimeError:
            missing.append(fname)
    return h.hexdigest(), missing


def infer_profile_for_version(version):
    ver_tuple = parse_version(version)
    if ver_tuple >= (3, 4):
        tag_style = "OBJECT_HEADER"
    elif ver_tuple >= (2, 14):
        tag_style = "CID_SHIFT1"
    else:
        tag_style = "CID_INT32"
    if (2, 16) <= ver_tuple < (2, 18):
        hdr = 6
    elif ver_tuple >= (2, 12):
        hdr = 5
    else:
        hdr = 4
    if ver_tuple < (2, 17) or (ver_tuple[0], ver_tuple[1]) in ((3, 2), (3, 4)):
        cws = 8
    else:
        cws = 4
    return {
        "cws": cws,
        "heap_object_tag": 1,
        "max_alignment": 16,
        "it_cap": 20000,
        "tag_style": tag_style,
        "header_fields": hdr,
    }


def header_style_for_version(version):
    ver_tuple = parse_version(version)
    if ver_tuple < (2, 12):
        return HEADER_STYLE_NAMES["2.10"]
    if ver_tuple < (2, 14):
        return HEADER_STYLE_NAMES["2.12"]
    if ver_tuple < (2, 16):
        return HEADER_STYLE_NAMES["2.14"]
    if ver_tuple < (2, 18):
        return HEADER_STYLE_NAMES["2.16"]
    return HEADER_STYLE_NAMES["modern"]


def ref_encoding_for_version(version):
    if parse_version(version) < (2, 18):
        return REF_ENCODING_NAMES["unsigned"]
    return REF_ENCODING_NAMES["ref_id128"]


def pool_encoding_for_version(version):
    ver_tuple = parse_version(version)
    if ver_tuple < (3, 2):
        return POOL_ENCODING_NAMES["low7"]
    if ver_tuple < (3, 3):
        return POOL_ENCODING_NAMES["low7_swapped"]
    return POOL_ENCODING_NAMES["modern"]


def code_layout_for_version(version):
    ver_tuple = parse_version(version)
    return {
        "alloc": CODE_ALLOC_NAMES["deferred_only" if ver_tuple < (2, 14) else "state_deferred"],
        "refs": 7 if ver_tuple < (2, 17) else 6,
        "leading_refs": 2 if ver_tuple >= (3, 13) else 0,
        "has_text_offset": "true" if ver_tuple < (2, 16) else "false",
    }


def snapshot_layout_for_version(version):
    parsed = parse_version(version)
    modern_single = parsed >= (3, 13)
    return {
        "single": "true" if modern_single else "false",
        "class_fixed": "true" if modern_single else "false",
        "closure_variable": "true" if modern_single else "false",
        "class_top_level_cid20": "true" if parsed >= (2, 19) else "false",
    }


def profile_diff(existing, inferred):
    diffs = []
    for key in ("cws", "heap_object_tag", "max_alignment", "it_cap", "tag_style", "header_fields"):
        if existing.get(key) != inferred.get(key):
            diffs.append(f"{key}:{existing.get(key)}!={inferred.get(key)}")
    return diffs


def cid_signature(cids):
    keys = (
        "Class", "Function", "Code", "ObjectPool", "String",
        "OneByteString", "TwoByteString", "Array", "Mint",
        "NumPredefinedCids",
    )
    return tuple(cids.get(key) for key in keys)


def report_item(report, key, value):
    report.setdefault(key, []).append(value)


def profiles_from_offsets(offsets):
    profiles = offsets.get("profiles")
    if isinstance(profiles, dict):
        return profiles
    versions = offsets.get("versions")
    if isinstance(versions, dict):
        return versions
    return {}


def profile_layout(profile):
    return {key: profile[key] for key in LAYOUT_KEYS}


def cid_values_from_table(table):
    cids = table.get("cids", {})
    return {kind: cids[kind] for kind, _class_name in CID_KIND_TO_CLASS_NAME if kind in cids}


def profile_signature(profile):
    sig = profile_layout(profile)
    for key in CID_META_KEYS:
        sig[key] = profile.get(key, 0)
    sig["cids"] = cid_values_from_table(profile)
    return sig


def profile_matches(a, b):
    return bool(a and b and profile_signature(a) == profile_signature(b))


def floor_profile(profiles, version):
    best = None
    for key in sorted(profiles.keys(), key=parse_version):
        if parse_version(key) > parse_version(version):
            break
        best = profiles[key]
    if best:
        return best
    keys = sorted(profiles.keys(), key=parse_version)
    return profiles[keys[0]] if keys else None


def make_profile(layout, cid_table, note=None):
    profile = dict(layout)
    for key in CID_META_KEYS:
        profile[key] = cid_table[key]
    profile["cids"] = cid_values_from_table(cid_table)
    if note:
        profile["note"] = note
    return profile


def cid_table_from_cids(cids):
    base, limit, stride = typed_data_info_from_cids(cids)
    return {
        "cids": {kind: cids[class_name] for kind, class_name in CID_KIND_TO_CLASS_NAME if class_name in cids},
        "typed_data_internal_base": base,
        "typed_data_internal_limit": limit,
        "typed_data_stride": stride,
    }


# ═══════════════════════════════════════════════════════════════════════
# C code generation from offsets.json
# ═══════════════════════════════════════════════════════════════════════

def gen_hash_entries(offsets):
    """Generate just the entries for known_hashes[] (no declaration wrapper)."""
    hashes = offsets.get("hashes", {})
    lines = []
    by_minor = {}
    for h, ver in sorted(hashes.items()):
        v = parse_version(ver)
        key = (v[0], v[1])
        by_minor.setdefault(key, []).append((h, ver))
    for minor_key in sorted(by_minor.keys()):
        lines.append(f"\t// Dart {minor_key[0]}.{minor_key[1]}.x")
        for h, ver in by_minor[minor_key]:
            lines.append(f'\t{{ "{h}", "{ver}" }},')
    lines.append("\t{ NULL, NULL }")
    lines.append("};")
    return "\n".join(lines)


def gen_profile_entries(offsets):
    """Generate just the entries for version_profiles[] (no declaration wrapper)."""
    profiles = profiles_from_offsets(offsets)
    lines = []
    for ver in sorted(profiles.keys(), key=parse_version):
        v = profiles[ver]
        tag_name = TAG_STYLE_NAMES.get(v["tag_style"], "0")
        header_style = header_style_for_version(ver)
        ref_encoding = ref_encoding_for_version(ver)
        pool_encoding = pool_encoding_for_version(ver)
        code = code_layout_for_version(ver)
        snapshot = snapshot_layout_for_version(ver)
        lines.append(f'\t{{ "", "{ver}", {v["cws"]}, {v["heap_object_tag"]}, '
                     f'{v["max_alignment"]}, {v["it_cap"]}, {tag_name}, '
                     f'{v["header_fields"]}, {header_style}, {ref_encoding}, {pool_encoding}, '
                     f'{code["alloc"]}, {code["refs"]}, {code["leading_refs"]}, {code["has_text_offset"]}, '
                     f'{snapshot["single"]}, {snapshot["class_fixed"]}, {snapshot["closure_variable"]}, '
                     f'{snapshot["class_top_level_cid20"]} }}, // Dart {ver}')
    lines.append("\t{ { 0 }, NULL, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }")
    lines.append("};")
    return "\n".join(lines)


def gen_dart_offsets_h(offsets):
    """Generate the dart_offsets.h content from offsets.json."""
    hashes = offsets.get("hashes", {})
    defaults = offsets.get("defaults", {})
    ep = defaults.get("code_entry_point_offsets", [8, 24, 16, 32])
    owner = defaults.get("code_owner_offsets", [56])
    name = defaults.get("function_name_offsets", [24, 8])
    profiles = profiles_from_offsets(offsets)

    lines = [
        "// Auto-generated from offsets.json by scripts/update-dart-version",
        "// Hash to compressed_word_size lookup table",
        "#ifndef R2C_DART_OFFSETS_H",
        "#define R2C_DART_OFFSETS_H",
        "",
        "#include <stdint.h>",
        "",
        "// Hash entry: snapshot MD5 hash (32 hex chars) + compressed_word_size",
        "typedef struct {",
        "\tconst char hash[33];",
        "\tint compressed_word_size;",
        "} DartHashEntry;",
        "",
        "static const DartHashEntry dart_hash_entries[] = {",
    ]
    for h, ver in sorted(hashes.items()):
        profile = floor_profile(profiles, ver)
        cws = profile.get("cws", 4) if profile else 4
        lines.append(f'\t{{ "{h}", {cws} }}, // Dart {ver}')
    lines.append("};")
    lines.append("")
    lines.append(f"#define DART_HASH_ENTRIES_COUNT {len(hashes)}")
    lines.append("")
    lines.append("// Default offset arrays (identical across all known Dart versions)")
    lines.append("#define DART_DEFAULT_EP_OFFSETS \\")
    lines.append(f"\t{{ {', '.join(str(x) for x in ep)} }}")
    lines.append(f"#define DART_DEFAULT_EP_OFFSETS_COUNT {len(ep)}")
    lines.append("#define DART_DEFAULT_OWNER_OFFSETS \\")
    lines.append(f"\t{{ {', '.join(str(x) for x in owner)} }}")
    lines.append(f"#define DART_DEFAULT_OWNER_OFFSETS_COUNT {len(owner)}")
    lines.append("#define DART_DEFAULT_NAME_OFFSETS \\")
    lines.append(f"\t{{ {', '.join(str(x) for x in name)} }}")
    lines.append(f"#define DART_DEFAULT_NAME_OFFSETS_COUNT {len(name)}")
    lines.append("")
    lines.append("#endif // R2C_DART_OFFSETS_H")
    lines.append("")
    return "\n".join(lines)


def gen_cid_table_entries(profiles):
    lines = []
    lines.append("\t// clang-format off")
    for ver in sorted(profiles.keys(), key=parse_version):
        cids = profiles[ver].get("cids", {})
        base = profiles[ver].get("typed_data_internal_base", 0)
        limit = profiles[ver].get("typed_data_internal_limit", 0)
        stride = profiles[ver].get("typed_data_stride", 0)
        cid_items = []
        for kind, class_name in CID_KIND_TO_CLASS_NAME:
            value = cids.get(kind)
            if value is not None:
                cid_items.append(f"CID ({kind}, {value})")
        lines.append(f'\t{{ "{ver}", {{ {", ".join(cid_items)} }}, {base}, {limit}, {stride} }},')
    lines.append('\t{ NULL, { 0 }, 0, 0, 0 }')
    lines.append("\t// clang-format on")
    return "\n".join(lines)


def write_dart_cid_c(filepath, profiles):
    """Rewrite cid_tables[] in dart_cid.c from parsed SDK class_id.h data."""
    if not profiles:
        return
    with open(filepath) as f:
        content = f.read()
    table_pos = content.find("cid_tables[]")
    if table_pos < 0:
        print("Error: could not find 'cid_tables[]' in file", file=sys.stderr)
        sys.exit(1)
    table_eq = content.find("=", table_pos)
    table_open = content.find("{", table_eq)
    names_pos = content.find("static const char *const cid_kind_names", table_open)
    if table_open < 0 or names_pos < 0:
        print("Error: could not locate cid_tables[] replacement bounds", file=sys.stderr)
        sys.exit(1)
    new_content = (
        content[:table_open + 1] +
        "\n" + gen_cid_table_entries(profiles) +
        "\n};\n\n" + content[names_pos:]
    )
    with open(filepath, 'w') as f:
        f.write(new_content)
    print(f"  Updated {filepath}")


def write_dart_version_c(filepath, offsets):
    """Rewrite known_hashes[] and version_profiles[] in dart_version.c."""
    hash_entries = gen_hash_entries(offsets)
    profile_entries = gen_profile_entries(offsets)

    with open(filepath) as f:
        content = f.read()

    # Find version_profiles[] bounds (comes later in the file)
    vp_pos = content.find("version_profiles[]")
    if vp_pos < 0:
        print("Error: could not find 'version_profiles[]' in file", file=sys.stderr)
        sys.exit(1)
    vp_eq = content.find("=", vp_pos)
    vp_open = content.find("{", vp_eq)
    vp_depth = 0
    vp_close = -1
    for i in range(vp_open, len(content)):
        if content[i] == '{':
            vp_depth += 1
        elif content[i] == '}':
            vp_depth -= 1
            if vp_depth == 0 and i + 1 < len(content) and content[i + 1] == ';':
                vp_close = i + 2
                break
    if vp_close < 0:
        print("Error: could not find closing '};' for version_profiles[]", file=sys.stderr)
        sys.exit(1)

    # Replace version_profiles[] first (it's later in the file)
    content = content[:vp_open + 1] + "\n" + profile_entries + "\n\n" + content[vp_close:].lstrip("\n")

    # Now find known_hashes[] bounds in the MODIFIED content
    kh_pos = content.find("known_hashes[]")
    if kh_pos < 0:
        print("Error: could not find 'known_hashes[]' in file", file=sys.stderr)
        sys.exit(1)
    kh_eq = content.find("=", kh_pos)
    kh_open = content.find("{", kh_eq)
    kh_depth = 0
    kh_close = -1
    for i in range(kh_open, len(content)):
        if content[i] == '{':
            kh_depth += 1
        elif content[i] == '}':
            kh_depth -= 1
            if kh_depth == 0 and i + 1 < len(content) and content[i + 1] == ';':
                kh_close = i + 2
                break
    if kh_close < 0:
        print("Error: could not find closing '};' for known_hashes[]", file=sys.stderr)
        sys.exit(1)

    # Replace known_hashes[]
    content = content[:kh_open + 1] + "\n" + hash_entries + "\n\n" + content[kh_close:].lstrip("\n")

    with open(filepath, 'w') as f:
        f.write(content)
    print(f"  Updated {filepath}")


def write_dart_offsets_h(filepath, offsets):
    """Rewrite include/r2flutter/dart_offsets.h from offsets.json."""
    content = gen_dart_offsets_h(offsets)
    with open(filepath, 'w') as f:
        f.write(content)
    print(f"  Generated {filepath}")


def sync_stable_tags(args, repo_root, offsets_path, dart_version_path, dart_offsets_h_path, dart_cid_path):
    tags = list_stable_sdk_tags(args.sdk_repo, args.min_version, args.max_version)
    if not tags:
        print("No stable Dart SDK tags found", file=sys.stderr)
        sys.exit(1)

    repo_cache = args.repo_cache
    temp_dir = None
    if not repo_cache:
        temp_dir = tempfile.TemporaryDirectory(prefix="r2flutter-dart-sdk-")
        repo_cache = os.path.join(temp_dir.name, "sdk")
    try:
        print(f"Found {len(tags)} stable Dart SDK tags ({tags[0]}..{tags[-1]})", flush=True)
        print(f"Using Dart SDK cache: {repo_cache}", flush=True)
        ensure_partial_sdk_repo(args.sdk_repo, repo_cache)
        fetch_sdk_tags(repo_cache, tags)

        offsets = load_offsets(offsets_path)
        hashes = offsets.setdefault("hashes", {})
        profiles = offsets.setdefault("profiles", profiles_from_offsets(offsets))
        offsets.pop("versions", None)
        candidate_profiles = {}
        report = {
            "new_hashes": [],
            "new_profiles": [],
            "hash_aliases": [],
            "hash_conflicts": [],
            "profile_mismatches": [],
            "cid_aliases": [],
            "missing_files": [],
            "parse_errors": [],
        }
        hash_cid_signatures = {}

        blob_paths = [f"runtime/vm/{fname}" for fname in VM_SNAPSHOT_FILES]
        class_id_path = "runtime/vm/class_id.h"
        tag_paths = blob_paths + [class_id_path]

        for idx, tag in enumerate(tags, start=1):
            print(f"[{idx:03d}/{len(tags):03d}] {tag}", flush=True)
            try:
                blobs, missing_paths = git_blob_bytes(repo_cache, tag, tag_paths)
            except RuntimeError as e:
                report_item(report, "parse_errors", (tag, str(e)))
                continue
            h = hashlib.md5()
            missing = []
            for fname, path in zip(VM_SNAPSHOT_FILES, blob_paths):
                data = blobs.get(path)
                if data is None:
                    missing.append(fname)
                    continue
                h.update(data)
            snapshot_hash = h.hexdigest()
            if class_id_path in missing_paths:
                missing.append("class_id.h")
            if missing:
                report_item(report, "missing_files", (tag, missing))

            known_version = hashes.get(snapshot_hash)
            if known_version:
                if known_version != tag:
                    report_item(report, "hash_aliases", (snapshot_hash, known_version, tag))
            else:
                hashes[snapshot_hash] = tag
                report_item(report, "new_hashes", (snapshot_hash, tag))

            try:
                class_id_data = blobs.get(class_id_path)
                if class_id_data is None:
                    raise ValueError("runtime/vm/class_id.h not found")
                class_id = class_id_data.decode()
                cids = parse_class_id_content(class_id)
            except Exception as e:
                report_item(report, "parse_errors", (tag, str(e)))
                continue
            cid_table = cid_table_from_cids(cids)
            candidate_profiles[tag] = make_profile(infer_profile_for_version(tag), cid_table)
            sig = cid_signature(cids)
            old_sig = hash_cid_signatures.setdefault(snapshot_hash, (tag, sig))
            if old_sig[1] != sig:
                report_item(report, "hash_conflicts", (snapshot_hash, old_sig[0], tag))

        for dst, src in CID_TABLE_VERSION_ALIASES.items():
            src_profile = candidate_profiles.get(src) or floor_profile(profiles, src)
            if dst in candidate_profiles and src_profile:
                layout = profile_layout(candidate_profiles[dst])
                candidate_profiles[dst] = make_profile(
                    layout,
                    src_profile,
                    "Uses fallback CIDs from 3.9.2 for the in-tree Flutter engine snapshot; not standalone Dart SDK 2.18.2 CIDs.")
                report_item(report, "cid_aliases", (dst, src))

        for tag in sorted(candidate_profiles.keys(), key=parse_version):
            candidate = candidate_profiles[tag]
            existing = profiles.get(tag)
            if existing:
                diffs = profile_diff(existing, candidate)
                if cid_values_from_table(existing) != cid_values_from_table(candidate):
                    diffs.append("cids")
                for key in CID_META_KEYS:
                    if existing.get(key) != candidate.get(key):
                        diffs.append(key)
                if diffs:
                    report_item(report, "profile_mismatches", (tag, diffs))
                if args.force_profiles:
                    profiles[tag] = candidate
                continue
            floor = floor_profile(profiles, tag)
            if not profile_matches(candidate, floor):
                profiles[tag] = candidate
                report_item(report, "new_profiles", tag)

        save_offsets(offsets_path, offsets)
        write_dart_version_c(dart_version_path, offsets)
        write_dart_offsets_h(dart_offsets_h_path, offsets)
        write_dart_cid_c(dart_cid_path, profiles)

        print("\nSync report:")
        print(f"  Tags scanned: {len(tags)}")
        print(f"  Range profiles: {len(profiles)}")
        print(f"  New hashes: {len(report['new_hashes'])}")
        print(f"  New range profiles: {len(report['new_profiles'])}")
        print(f"  Existing-hash aliases: {len(report['hash_aliases'])}")
        print(f"  Profile mismatches preserved: {len(report['profile_mismatches'])}")
        print(f"  CID table aliases applied: {len(report['cid_aliases'])}")
        print(f"  Hash/CID conflicts: {len(report['hash_conflicts'])}")
        print(f"  Tags with missing hash inputs: {len(report['missing_files'])}")
        print(f"  Parse/hash errors: {len(report['parse_errors'])}")
        if report["profile_mismatches"]:
            print("  Profile mismatches:")
            for tag, diffs in report["profile_mismatches"][:12]:
                print(f"    {tag}: {', '.join(diffs)}")
            if len(report["profile_mismatches"]) > 12:
                print(f"    ... {len(report['profile_mismatches']) - 12} more")
        if report["hash_conflicts"]:
            print("  Hash/CID conflicts:")
            for snapshot_hash, old_tag, new_tag in report["hash_conflicts"]:
                print(f"    {snapshot_hash}: {old_tag} vs {new_tag}")
        if report["cid_aliases"]:
            print("  CID table aliases:")
            for dst, src in report["cid_aliases"]:
                print(f"    {dst}: {src}")
        if report["missing_files"]:
            print("  Missing hash inputs:")
            for tag, files in report["missing_files"][:12]:
                print(f"    {tag}: {', '.join(files)}")
            if len(report["missing_files"]) > 12:
                print(f"    ... {len(report['missing_files']) - 12} more")
        if report["parse_errors"]:
            print("  Errors:")
            for tag, error in report["parse_errors"][:12]:
                print(f"    {tag}: {error}")
            if len(report["parse_errors"]) > 12:
                print(f"    ... {len(report['parse_errors']) - 12} more")
        print("\nDone. Run 'make && make test' to verify.")
    finally:
        if temp_dir:
            temp_dir.cleanup()


# ═══════════════════════════════════════════════════════════════════════
# Main
# ═══════════════════════════════════════════════════════════════════════

def main():
    parser = argparse.ArgumentParser(
        description="Unified Dart version table generator for r2flutter")
    parser.add_argument("version", nargs="?", help="Dart SDK version (e.g. 3.13.0)")
    parser.add_argument("--sdk-dir", help="Path to local Dart SDK checkout")
    parser.add_argument("--sdk-repo", default="https://github.com/dart-lang/sdk.git",
                        help="Dart SDK git repo URL (default: %(default)s)")
    parser.add_argument("--hash", help="Snapshot hash (skip SDK pull)")
    parser.add_argument("--generate", action="store_true",
                        help="Regenerate C files from offsets.json (no SDK pull)")
    parser.add_argument("--sync-stable-tags", action="store_true",
                        help="Pull stable Dart SDK tags and rebuild hash/profile/CID tables")
    parser.add_argument("--min-version", default="2.10.0",
                        help="Oldest stable Dart SDK tag to sync (default: %(default)s)")
    parser.add_argument("--max-version",
                        help="Newest stable Dart SDK tag to sync")
    parser.add_argument("--repo-cache",
                        help="Path to a reusable partial Dart SDK git cache")
    parser.add_argument("--force-profiles", action="store_true",
                        help="Overwrite existing version profiles with inferred defaults")
    parser.add_argument("--list", action="store_true",
                        help="List known versions and exit")
    args = parser.parse_args()

    repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    offsets_path = os.path.join(repo_root, "offsets.json")
    dart_version_path = os.path.join(repo_root, "src", "lib", "dart_version.c")
    dart_offsets_h_path = os.path.join(repo_root, "include", "r2flutter", "dart_offsets.h")
    dart_cid_path = os.path.join(repo_root, "src", "lib", "dart_cid.c")

    # ── --list mode ──────────────────────────────────────────────────
    if args.list:
        offsets = load_offsets(offsets_path)
        hashes = offsets.get("hashes", {})
        print("Known Dart versions and snapshot hashes:")
        for h, v in sorted(hashes.items(), key=lambda x: (parse_version(x[1]), x[0])):
            print(f"  {v:10s}  {h}")
        return

    # ── --generate mode ───────────────────────────────────────────────
    if args.generate:
        print("Regenerating C files from offsets.json...")
        offsets = load_offsets(offsets_path)
        write_dart_version_c(dart_version_path, offsets)
        write_dart_offsets_h(dart_offsets_h_path, offsets)
        write_dart_cid_c(dart_cid_path, profiles_from_offsets(offsets))
        print("Done. Run 'make && make test' to verify.")
        return

    # ── --sync-stable-tags mode ───────────────────────────────────────
    if args.sync_stable_tags:
        sync_stable_tags(args, repo_root, offsets_path, dart_version_path, dart_offsets_h_path, dart_cid_path)
        return

    # ── Need a version argument ───────────────────────────────────────
    if not args.version:
        parser.print_help()
        sys.exit(1)

    version = args.version
    parsed_cids = None

    # ── Get or compute the snapshot hash ──────────────────────────────
    if args.hash:
        snapshot_hash = args.hash
        print(f"Using provided hash: {snapshot_hash}")
    elif args.sdk_dir:
        vm_dir = os.path.join(args.sdk_dir, "runtime", "vm")
        if not os.path.isdir(vm_dir):
            print(f"Error: runtime/vm not found in {args.sdk_dir}", file=sys.stderr)
            sys.exit(1)
        snapshot_hash = compute_snapshot_hash(vm_dir)
        print(f"Computed snapshot hash from {vm_dir}: {snapshot_hash}")
        class_id_path = os.path.join(vm_dir, "class_id.h")
        if os.path.exists(class_id_path):
            parsed_cids = parse_class_id_h(class_id_path)
            print(f"Extracted CIDs from class_id.h ({len(parsed_cids)} entries)")
    else:
        print(f"Cloning Dart SDK tag {version}...")
        with tempfile.TemporaryDirectory(prefix=f"dart-sdk-{version}-") as tmpdir:
            vm_dir = pull_sdk_tag(args.sdk_repo, version, tmpdir)
            if not vm_dir:
                print(f"Error: could not fetch SDK tag {version}", file=sys.stderr)
                sys.exit(1)
            snapshot_hash = compute_snapshot_hash(vm_dir)
            print(f"Computed snapshot hash: {snapshot_hash}")

            class_id_path = os.path.join(vm_dir, "class_id.h")
            if os.path.exists(class_id_path):
                parsed_cids = parse_class_id_h(class_id_path)
                print(f"Extracted CIDs from class_id.h ({len(parsed_cids)} entries)")
                print(f"  Class={parsed_cids.get('Class', '?')}, Function={parsed_cids.get('Function', '?')}, "
                      f"Code={parsed_cids.get('Code', '?')}, String={parsed_cids.get('String', '?')}")
            else:
                print("Warning: class_id.h not found")

    # ── Update offsets.json ──────────────────────────────────────────
    offsets = load_offsets(offsets_path)
    hashes = offsets.setdefault("hashes", {})
    profiles = offsets.setdefault("profiles", profiles_from_offsets(offsets))
    offsets.pop("versions", None)

    if snapshot_hash in hashes:
        print(f"  Hash {snapshot_hash} already known (version {hashes[snapshot_hash]})")
    else:
        hashes[snapshot_hash] = version
        print(f"  Added hash {snapshot_hash} -> {version}")

    if parsed_cids:
        candidate = make_profile(infer_profile_for_version(version), cid_table_from_cids(parsed_cids))
        if not profile_matches(candidate, floor_profile(profiles, version)):
            profiles[version] = candidate
            print(f"  Added range profile: {version}")
    elif not floor_profile(profiles, version):
        print("Warning: no floor profile exists and no class_id.h was parsed")

    save_offsets(offsets_path, offsets)

    # ── Regenerate C files ──────────────────────────────────────────
    write_dart_version_c(dart_version_path, offsets)
    write_dart_offsets_h(dart_offsets_h_path, offsets)
    write_dart_cid_c(dart_cid_path, profiles)

    print("\nDone. Run 'make && make test' to verify.")


if __name__ == "__main__":
    main()
