#!/usr/bin/env python3
"""Best-effort static placeholder/parameter validator for 2.6.1 PHP queries.

It validates calls whose SQL and parameter array are both syntactically
literal. Calls using pre-built argument arrays are reported as skipped and are
covered by schema-contract review.
"""
from __future__ import annotations
import ast
import re
import sys
from pathlib import Path

ROOT=Path(__file__).resolve().parents[1]
FILES=[
 ROOT/'app/enterprise_authoritative_floorplans_261.php',
 ROOT/'app/enterprise_site_context.php',
 ROOT/'app/enterprise_release_261.php',
 ROOT/'public_html/system_hospital_room_editor.php',
]
FUNCS=('balt_exec','balt_insert','balt_one','balt_all','balt_value')


def scan_balanced(text:str,start:int,open_ch:str,close_ch:str)->tuple[str,int]:
    assert text[start]==open_ch
    depth=0; quote=None; esc=False
    for i in range(start,len(text)):
        ch=text[i]
        if quote:
            if esc: esc=False
            elif ch=='\\': esc=True
            elif ch==quote: quote=None
            continue
        if ch in ('"',"'"): quote=ch; continue
        if ch==open_ch: depth+=1
        elif ch==close_ch:
            depth-=1
            if depth==0:return text[start:i+1],i+1
    raise ValueError('unbalanced expression')


def split_top(expr:str,sep=',')->list[str]:
    out=[]; start=0; depths={'(':0,'[':0,'{':0}; pairs={')':'(',']':'[','}':'{'}; quote=None; esc=False
    for i,ch in enumerate(expr):
        if quote:
            if esc: esc=False
            elif ch=='\\': esc=True
            elif ch==quote: quote=None
            continue
        if ch in ('"',"'"): quote=ch; continue
        if ch in depths: depths[ch]+=1
        elif ch in pairs: depths[pairs[ch]]-=1
        elif ch==sep and not any(depths.values()):
            out.append(expr[start:i].strip()); start=i+1
    out.append(expr[start:].strip())
    return out


def php_string_value(expr:str)->str|None:
    expr=expr.strip()
    # Only a single quoted PHP literal; interpolation is retained as text.
    if len(expr)<2 or expr[0] not in ('"',"'") or expr[-1]!=expr[0]: return None
    body=expr[1:-1]
    if expr[0]=="'":
        return body.replace("\\'","'").replace('\\\\','\\')
    # enough for counting question marks
    return re.sub(r'\\([\\"nrt$])',lambda m:{'n':'\n','r':'\r','t':'\t'}.get(m.group(1),m.group(1)),body)


def array_count(expr:str)->int|None:
    expr=expr.strip()
    if not (expr.startswith('[') and expr.endswith(']')): return None
    inner=expr[1:-1].strip()
    if not inner:return 0
    return len(split_top(inner))

checks=[]; scanned=0; skipped=0
for path in FILES:
    text=path.read_text(encoding='utf-8',errors='ignore')
    for fn in FUNCS:
        pos=0
        token=fn+'('
        while True:
            idx=text.find(token,pos)
            if idx<0:break
            start=idx+len(fn)
            try: call,end=scan_balanced(text,start,'(',')')
            except ValueError:
                checks.append((f'{path.name}:{fn}@{idx}',False,'unbalanced call'))
                break
            args=split_top(call[1:-1])
            if len(args)<2:
                skipped+=1; pos=end; continue
            sql=php_string_value(args[0]); count=array_count(args[1])
            if sql is None or count is None:
                skipped+=1; pos=end; continue
            scanned+=1
            placeholders=sql.count('?')
            checks.append((f'{path.name}:{fn}@{text.count(chr(10),0,idx)+1}',placeholders==count,f'placeholders={placeholders}, params={count}'))
            pos=end

failed=[c for c in checks if not c[1]]
for key,ok,detail in checks:
    print(('PASS' if ok else 'FAIL')+'\t'+key+'\t'+detail)
print(f'SCANNED\t{scanned}')
print(f'SKIPPED_DYNAMIC\t{skipped}')
print(f'SUMMARY\t{len(checks)-len(failed)}/{len(checks)} PASS')
sys.exit(1 if failed else 0)
