"""Tokenizer-based PHP sanity linter: balanced ()[]{} outside strings/comments/heredocs,
'{' where a value is expected, PHP-tag-aware. Catches the common syntax breakers without PHP."""
import sys, glob, re, os
ROOT = sys.argv[1] if len(sys.argv) > 1 else '.'
errs = []

def php_spans(src):
    spans=[]; i=0; n=len(src); inphp=False; start=0
    while i<n:
        if not inphp:
            o=src.find('<?',i)
            if o==-1: break
            if src[o:o+5].startswith('<?php'): i=o+5
            elif src[o:o+3]=='<?=': i=o+3
            else: i=o+2
            inphp=True; start=i
        else:
            j=i
            while j<n:
                c=src[j]
                if c in "'\"":
                    q=c; j+=1
                    while j<n and src[j]!=q:
                        if src[j]=='\\': j+=1
                        j+=1
                    j+=1; continue
                if src.startswith('//',j) or (c=='#' and not src.startswith('#[',j)):
                    nl=src.find('\n',j); j=n if nl==-1 else nl; continue
                if src.startswith('/*',j): e=src.find('*/',j+2); j=n if e==-1 else e+2; continue
                if src.startswith('<<<',j):
                    m=re.match(r"<<<[ \t]*(['\"]?)([A-Za-z_]\w*)\1\r?\n", src[j:])
                    if m:
                        tg=m.group(2); j+=m.end()
                        cm=re.search(r"\n[ \t]*"+re.escape(tg)+r"\b", src[j:]); j=n if not cm else j+cm.end(); continue
                if src.startswith('?>',j): break
                j+=1
            spans.append((start,j)); i=j+2; inphp=False
    if inphp: spans.append((start,n))
    return spans

def lint(path):
    full=open(path,encoding='utf-8').read()
    for a,b in php_spans(full):
        lint_span(full,a,b,path,full.count('\n',0,a)+1)

def lint_span(full,a,b,path,base_line):
    s=full[a:b]; n=len(s); i=0; line=base_line; stack=[]; expect_value=False
    while i<n:
        c=s[i]
        if c=='\n': line+=1; i+=1; continue
        if c in ' \t\r': i+=1; continue
        if s.startswith('//',i) or (c=='#' and not s.startswith('#[',i)):
            nl=s.find('\n',i); i=n if nl==-1 else nl; continue
        if s.startswith('/*',i):
            e=s.find('*/',i+2)
            if e==-1: errs.append(f'{path}:{line}: unterminated comment'); return
            line+=s.count('\n',i,e); i=e+2; continue
        m=re.match(r"<<<[ \t]*(['\"]?)([A-Za-z_]\w*)\1\r?\n", s[i:])
        if m:
            tag=m.group(2); i+=m.end()
            cm=re.search(r"\n[ \t]*"+re.escape(tag)+r"\b", s[i:])
            if not cm: errs.append(f'{path}:{line}: unterminated heredoc {tag}'); return
            line+=s.count('\n',i,i+cm.end()); i+=cm.end(); expect_value=False; continue
        if c in "'\"":
            q=c; j=i+1
            while j<n and s[j]!=q:
                if s[j]=='\\': j+=1
                j+=1
            if j>=n: errs.append(f'{path}:{line}: unterminated string'); return
            line+=s.count('\n',i,j); i=j+1; expect_value=False; continue
        if c in '([':
            stack.append((c,line)); i+=1; expect_value=(c=='['); continue
        if c=='{':
            if expect_value: errs.append(f'{path}:{line}: unexpected "{{" where a value is expected'); return
            stack.append(('{',line)); i+=1; expect_value=False; continue
        if c in ')]}':
            pair={')':'(',']':'[','}':'{'}[c]
            if not stack or stack[-1][0]!=pair:
                errs.append(f'{path}:{line}: unbalanced "{c}"'+(f' (open {stack[-1][0]} line {stack[-1][1]})' if stack else '')); return
            stack.pop(); i+=1; expect_value=False; continue
        if c=='=':
            if s[i:i+2]=='=>': expect_value=True; i+=2; continue
            if s[i:i+3] in ('===','!=='): i+=3; expect_value=True; continue
            if s[i:i+2] in ('==','!=','<=','>=','+=','-=','.=','*=','/=','??'): i+=2; expect_value=True; continue
            expect_value=True; i+=1; continue
        if c in ',:': expect_value=(c==','); i+=1; continue
        if c==';': expect_value=False; i+=1; continue
        m=re.match(r'[A-Za-z_\\][\w\\]*', s[i:])
        if m:
            w=m.group(0); i+=m.end()
            expect_value = w in ('return','echo','print','yield','throw','and','or','new','fn','case','clone','instanceof')
            continue
        expect_value = c in '<>+-*/%.!?&|^~@'
        i+=1
    if stack:
        c,ln=stack[-1]; errs.append(f'{path}: unclosed "{c}" opened at line {ln}')

files = glob.glob(ROOT+'/src/**/*.php',recursive=True)+glob.glob(ROOT+'/public/*.php')+glob.glob(ROOT+'/bin/*.php')+glob.glob(ROOT+'/config/*.php')
for f in files: lint(f)
print('\n'.join(errs) if errs else 'NO PHP SYNTAX ISSUES')
print(f'{len(files)} PHP files tokenized')
