#!/usr/bin/env python3
"""Static placeholder/parameter count check for literal Baltique DB calls."""
from __future__ import annotations
from pathlib import Path
import json, re, sys
ROOT = Path(__file__).resolve().parents[1]
FUNCTIONS = ('balt_exec','balt_insert','balt_all','balt_one','balt_value')

def extract_balanced(text: str, start: int) -> tuple[str,int] | None:
    # start points at opening '('
    depth=0; quote=None; esc=False; i=start
    while i<len(text):
        ch=text[i]
        if quote:
            if esc: esc=False
            elif ch=='\\': esc=True
            elif ch==quote: quote=None
        else:
            if ch in "'\"": quote=ch
            elif ch=='(': depth+=1
            elif ch==')':
                depth-=1
                if depth==0: return text[start+1:i],i+1
        i+=1
    return None

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

def literal_string(expr: str) -> str | None:
    expr=expr.strip()
    if len(expr)<2 or expr[0] not in "'\"" or expr[-1]!=expr[0]: return None
    # skip interpolation/concatenation that changes placeholder count dynamically
    if expr[0]=='"' and re.search(r'(?<!\\)\$|\{\$',expr[1:-1]): return None
    if re.search(r"['\"]\s*\.",expr): return None
    body=expr[1:-1]
    # sufficient for ? count; only unescape escaped quote/backslash
    return body.replace('\\'+expr[0],expr[0]).replace('\\\\','\\')

def array_items(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))

files=[ROOT/'app/enterprise_clinical_go_live_program_250.php'] + sorted((ROOT/'public_html').glob('*250.php'))
checked=[]; mismatches=[]; skipped=[]
for path in files:
    text=path.read_text(encoding='utf-8')
    for fn in FUNCTIONS:
        for m in re.finditer(r'\b'+re.escape(fn)+r'\s*\(',text):
            res=extract_balanced(text,text.find('(',m.start()))
            if not res: continue
            content,_=res; args=split_top(content)
            if not args: continue
            sql=literal_string(args[0])
            if sql is None or len(args)<2:
                skipped.append({'file':str(path.relative_to(ROOT)),'function':fn,'reason':'dynamic/no params'})
                continue
            n=array_items(args[1])
            if n is None:
                skipped.append({'file':str(path.relative_to(ROOT)),'function':fn,'reason':'non-literal params'})
                continue
            q=sql.count('?')
            item={'file':str(path.relative_to(ROOT)),'function':fn,'placeholders':q,'params':n,'sql':sql[:180]}
            checked.append(item)
            if q!=n: mismatches.append(item)
result={'checked':len(checked),'skipped':len(skipped),'mismatches':mismatches}
print(json.dumps(result,ensure_ascii=False,indent=2))
sys.exit(1 if mismatches else 0)
