<?php
declare(strict_types=1);
require_once __DIR__.'/../old-driver-shadow-fetch-prototype/ShadowCore.php';

function virtualSchema(): array
{
    return ['version'=>'virtual-only-v1','table'=>'cdx_shadow_single','source'=>'offline synthetic contract, not real database metadata','columns'=>[
        ['ordinal'=>1,'name'=>'id','native_type'=>'int','rule'=>null],
        ['ordinal'=>2,'name'=>'wide_id','native_type'=>'decimal(20,0)','rule'=>['kind'=>'unsigned_integer','precision'=>0,'scale'=>0]],
        ['ordinal'=>3,'name'=>'amount','native_type'=>'decimal(38,18)','rule'=>['kind'=>'decimal_text','precision'=>38,'scale'=>18]],
        ['ordinal'=>4,'name'=>'ratio','native_type'=>'double','rule'=>null],
        ['ordinal'=>5,'name'=>'label','native_type'=>'varchar(32)','rule'=>null],
        ['ordinal'=>6,'name'=>'n','native_type'=>'decimal(30,2)','rule'=>['kind'=>'decimal_text','precision'=>30,'scale'=>2]],
    ]];
}

/** Code-owned offline subset; never accepts or parses an arbitrary SQL string. */
final class ProjectionContract
{
    public static function compile(array $options,array $schema,bool $allowStar): ?array
    {
        if(($options['table']??null)!==($schema['table']??null)||!self::identifier($schema['table'])||empty($schema['version'])||empty($schema['source'])){throw new InvalidArgumentException('Single table contract required');}
        if(!in_array($options['operation']??null,['select','find','value','column','cursor'],true)){throw new InvalidArgumentException('Trusted read operation required');}
        foreach(['join','union','group','having','distinct','lock','procedure','view','extra','force','partition','duplicate'] as $key){if(!empty($options[$key])){throw new InvalidArgumentException('Unsupported structural option');}}
        self::noObjects($options);
        $columns=[];$ordinal=1;
        foreach($schema['columns']??[] as $col){
            $name=$col['name']??null;if(!self::identifier($name)||isset($columns[$name])||($col['ordinal']??null)!==$ordinal++||!array_key_exists('rule',$col)||!isset($col['native_type'])){throw new InvalidArgumentException('Incomplete ordered schema');}
            if($col['rule']!==null){ShadowValue::restore(null,$col['rule']);}$columns[$name]=$col;
        }
        if(!$columns){throw new InvalidArgumentException('Empty schema');}
        $aliases=$options['alias']??[];
        if(!is_array($aliases)||count($aliases)>1||($aliases&&array_keys($aliases)!==[$schema['table']])){throw new InvalidArgumentException('Single alias mapping required');}
        $qualifier=$aliases[$schema['table']]??$schema['table'];if(!self::identifier($qualifier)){throw new InvalidArgumentException('Invalid table alias');}
        $fields=$options['field']??['*'];if($fields==='*'){$fields=['*'];}
        if(!is_array($fields)||!$fields){throw new InvalidArgumentException('Structured fields required');}
        if($fields===['*']||$fields===[$qualifier.'.*']){
            if(!$allowStar||($schema['star_allowed']??true)!==true){throw new InvalidArgumentException('Star requires matching ordered full contract');}$fields=array_keys($columns);
        }
        $out=[];$names=[];$rules=[];
        foreach($fields as $source=>$alias){
            if(is_int($source)){$source=$alias;$alias=null;}
            if(!is_string($source)||!preg_match('/^(?:([A-Za-z_][A-Za-z0-9_]*)\.)?([A-Za-z_][A-Za-z0-9_]*)$/D',$source,$m)){throw new InvalidArgumentException('Direct identifier only');}
            $prefix=$m[1]??'';$field=$m[2];
            if(($prefix!==''&&!in_array($prefix,[$qualifier,$schema['table']],true))||!isset($columns[$field])||($columns[$field]['source_present']??true)!==true){throw new InvalidArgumentException('Unknown field lineage');}
            $alias=$alias??$field;
            if(!self::identifier($alias)||in_array($alias,$names,true)){throw new InvalidArgumentException('Ambiguous visible alias');}
            $out[]=['source'=>$qualifier.'.'.$field,'alias'=>$alias];$names[]=$alias;$rules[]=$columns[$field]['rule'];
        }
        foreach($options['order']??[] as $field=>$direction){
            if(is_int($field)){$field=$direction;$direction='ASC';}
            if(!is_string($field)||!preg_match('/^(?:([A-Za-z_][A-Za-z0-9_]*)\.)?([A-Za-z_][A-Za-z0-9_]*)$/D',$field,$m)||!in_array(strtoupper((string)$direction),['ASC','DESC'],true)){throw new InvalidArgumentException('Structured order only');}
            if(!isset($columns[$m[2]])&&!in_array($m[2],$names,true)){throw new InvalidArgumentException('Unknown order column');}
            if(($m[1]??'')!==''&&!in_array($m[1],[$qualifier,$schema['table']],true)){throw new InvalidArgumentException('Cross-table order');}
        }
        $visible=range(0,count($out)-1);$restore=[];$helpers=[];
        foreach($rules as $i=>$rule){if($rule===null){continue;}$name='cdx_shadow_'.count($helpers);if(in_array($name,$names,true)){throw new InvalidArgumentException('Helper collision');}
            $restore[$i]=['shadow'=>count($names),'rule'=>$rule];$names[]=$name;$helpers[]=['source'=>$out[$i]['source'],'alias'=>$name];}
        if(!$restore){return null;}
        return ['columns'=>$out,'helpers'=>$helpers,'names'=>$names,'visible'=>$visible,'restore'=>$restore,'schema_digest'=>hash('sha256',json_encode($schema))];
    }
    private static function identifier($name): bool {return is_string($name)&&preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/D',$name)===1;}
    private static function noObjects(array $tree): void {foreach($tree as $value){if(is_object($value)||is_resource($value)){throw new InvalidArgumentException('Raw/object options unsupported');}if(is_array($value)){self::noObjects($value);}}}
}
