"""Fix POST files: add body. prefix to bare body field references in code contexts.
Handles both snake_case and single-word field names, excluding object keys and strings."""
import os, json, re

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
API_DIR = os.path.join(SCRIPT_DIR, 'data', 'magic-api', 'api')

def parse_ms_file(filepath):
    with open(filepath, 'r', encoding='utf-8') as f:
        content = f.read()
    parts = content.split('================================\n', 1)
    if len(parts) != 2:
        return None, None
    try:
        header = json.loads(parts[0].strip())
    except Exception:
        return None, None
    return header, parts[1]

def write_ms_file(filepath, header, script):
    new_content = json.dumps(header, ensure_ascii=False, indent=2)
    new_content += '\n================================\n' + script
    with open(filepath, 'w', encoding='utf-8') as f:
        f.write(new_content)

def get_body_fields_and_locals(script):
    """Extract body field names and local variable names from script."""
    body_fields = set()
    local_vars = set()

    # From var xxx = body.yyy; declarations
    for m in re.finditer(r'var\s+(\w+)\s*=\s*body\.(\w+)\s*;', script):
        local_vars.add(m.group(1))
        body_fields.add(m.group(2))

    # From existing body.xxx references
    for m in re.finditer(r'(?<!\w)body\.(\w+)', script):
        body_fields.add(m.group(1))

    # Other var declarations
    for m in re.finditer(r'var\s+(\w+)', script):
        local_vars.add(m.group(1))

    # for-in loop variables
    for m in re.finditer(r'for\s*\(\s*(\w+)\s+in\s+', script):
        local_vars.add(m.group(1))

    # Remove body itself
    body_fields.discard('body')

    return body_fields, local_vars

def get_code_ranges(script):
    """Find code ranges (outside string literals). Returns list of (start, end) tuples."""
    ranges = []
    pos = 0
    in_string = False
    string_start = 0
    i = 0

    while i < len(script):
        if not in_string:
            # Check for triple-quote start
            if script[i:i+3] in ('"""', "'''"):
                if pos < i:
                    ranges.append((pos, i))
                in_string = True
                string_start = i
                i += 3
                continue
            # Check for single-quote string
            if script[i] == "'" and script[i:i+3] not in ("'''",):
                if pos < i:
                    ranges.append((pos, i))
                # Find end of single-quote string
                j = i + 1
                while j < len(script):
                    if script[j] == '\\':
                        j += 2
                        continue
                    if script[j] == "'":
                        break
                    j += 1
                i = j + 1
                pos = i
                continue
            i += 1
        else:
            # In triple-quoted string, look for end
            if script[i:i+3] in ('"""', "'''"):
                in_string = False
                pos = i + 3
                i += 3
                continue
            i += 1

    if pos < len(script) and not in_string:
        ranges.append((pos, len(script)))

    return ranges

# Keywords that should NOT be treated as body fields
KEYWORDS = {
    'true', 'false', 'null', 'now', 'new', 'return', 'if', 'else',
    'for', 'in', 'var', 'function', 'import', 'exit', 'assert',
    'db', 'StpUtil', 'BCryptPasswordEncoder',
    'getLoginIdAsLong', 'getTokenInfo', 'transaction',
    'select', 'selectOne', 'selectInt', 'update', 'insert',
    'size', 'push', 'remove', 'matches', 'encode', 'login',
    'body',
}

fixed = 0
for root, dirs, files in os.walk(API_DIR):
    for fname in sorted(files):
        if not fname.endswith('.ms'):
            continue
        fpath = os.path.join(root, fname)
        header, script = parse_ms_file(fpath)
        if header is None or header.get('method') != 'POST':
            continue

        body_fields, local_vars = get_body_fields_and_locals(script)

        # Also discover body fields from assertion/condition patterns
        code_text = ''.join(
            script[s:e] for s, e in get_code_ranges(script)
        )

        # Discover from assertions: assert xxx != null
        for m in re.finditer(r'assert\s+(\w+)\s*[!=<>]', code_text):
            name = m.group(1)
            if name not in local_vars and name not in KEYWORDS:
                body_fields.add(name)

        # Discover from if conditions: if (xxx != null)
        for m in re.finditer(r'if\s*\(\s*(\w+)\s*[!=<>]', code_text):
            name = m.group(1)
            if name not in local_vars and name not in KEYWORDS:
                body_fields.add(name)

        if not body_fields:
            continue

        old_script = script

        # Sort by length desc to avoid partial matches
        sorted_fields = sorted(body_fields, key=lambda f: (-len(f), f))

        # Get code ranges and fix only those
        ranges = get_code_ranges(script)
        # Build new script by processing each range
        result = []
        last_end = 0
        for start, end in ranges:
            result.append(script[last_end:start])  # string content before this range
            code = script[start:end]

            for field in sorted_fields:
                # Match bare field name:
                # - NOT preceded by 'body.'
                # - NOT preceded by word char, ., $, {, #
                # - NOT followed by word char or :
                code = re.sub(
                    r'(?<!body\.)'
                    r'(?<![\w.\${#])'
                    r'(' + re.escape(field) + r')'
                    r'(?![\w:])',
                    r'body.\1',
                    code
                )
            result.append(code)
            last_end = end

        result.append(script[last_end:])
        script = ''.join(result)

        # Fix edge cases: { body.xxx: ... } → { xxx: ... } (object keys)
        for field in sorted_fields:
            script = re.sub(
                r'\{\s*body\.' + re.escape(field) + r'\s*:',
                r'{ ' + field + ' :',
                script
            )

        if script != old_script:
            write_ms_file(fpath, header, script)
            fixed += 1
            print(f'  Fixed POST: {os.path.relpath(fpath, SCRIPT_DIR)}')

print(f'\nPOST files fixed: {fixed}')
