<?php
declare(strict_types=1);

final class ShadowMode
{
    // PHP 7 PDO uses numeric 0 for the optional/default mode; no FETCH_DEFAULT constant.
    public const DEFAULT_MODE=0;
    public static function row($mode,int $default): int
    {
        if($mode===null||$mode===self::DEFAULT_MODE){$mode=$default;}
        if(!in_array($mode,[PDO::FETCH_NUM,PDO::FETCH_ASSOC,PDO::FETCH_BOTH,PDO::FETCH_OBJ],true)){throw new InvalidArgumentException('Unsupported planned row mode');}
        return $mode;
    }
}

/** Synthetic-only prototype. Never accepts a float as an exact source. */
final class ShadowValue
{
    public static function restore($text,array $rule)
    {
        $kind=$rule['kind']??'';
        if(!in_array($kind,['decimal_text','signed_integer','unsigned_integer'],true)){throw new InvalidArgumentException('Unknown trusted type');}
        if($kind==='decimal_text'){
            $p=$rule['precision']??null;$s=$rule['scale']??null;
            if(!is_int($p)||!is_int($s)||$p<1||$p>38||$s<0||$s>$p){throw new InvalidArgumentException('Invalid declared decimal');}
        }
        if($text===null){return null;}
        if(!is_string($text)||!preg_match('/^(-?)([0-9]+)(?:\.([0-9]+))?$/D',$text,$m)){throw new UnexpectedValueException('Exact plain text required');}
        $whole=ltrim($m[2],'0');$whole=$whole===''?'0':$whole;$fraction=$m[3]??'';
        if($kind==='decimal_text'){
            if(strlen($fraction)>$s && trim(substr($fraction,$s),'0')!==''){throw new UnexpectedValueException('Nonzero scale truncation');}
            $fraction=str_pad(substr($fraction,0,$s),$s,'0');
            $wholeDigits=$whole==='0'?0:strlen($whole);
            if($wholeDigits>$p-$s){throw new UnexpectedValueException('Decimal precision overflow');}
            $sign=$whole==='0'&&trim($fraction,'0')===''?'':$m[1];
            return $sign.$whole.($s>0?'.'.$fraction:'');
        }
        if(isset($m[3])){throw new UnexpectedValueException('Integer text required');}
        $negative=$m[1]==='-'&&$whole!=='0';
        if($kind==='unsigned_integer'&&$negative){throw new UnexpectedValueException('Unsigned negative');}
        $limit=$kind==='unsigned_integer'?'18446744073709551615':($negative?'9223372036854775808':'9223372036854775807');
        if(self::above($whole,$limit)){throw new UnexpectedValueException('Declared integer range overflow');}
        $phpLimit=$negative?substr((string)PHP_INT_MIN,1):(string)PHP_INT_MAX;
        $normalized=($negative?'-':'').$whole;
        return self::above($whole,$phpLimit)?$normalized:(int)$normalized;
    }

    private static function above(string $digits,string $limit): bool
    {return strlen($digits)>strlen($limit)||(strlen($digits)===strlen($limit)&&strcmp($digits,$limit)>0);}
}

/** Immutable, code-owned exact SQL/position/semantic contract, NOT inferred metadata. */
final class ShadowPlan
{
    private $sql;private $names;private $visible;private $restore;
    public function __construct(array $spec)
    {
        $this->sql=$spec['sql']??null;$this->names=$spec['names']??null;$this->visible=$spec['visible']??null;$this->restore=$spec['restore']??null;
        if(!is_string($this->sql)||$this->sql===''||!is_array($this->names)||!is_array($this->visible)||!is_array($this->restore)||!$this->restore||!$this->visible){throw new InvalidArgumentException('Incomplete plan');}
        if(array_keys($this->names)!==range(0,count($this->names)-1)||array_keys($this->visible)!==range(0,count($this->visible)-1)){throw new InvalidArgumentException('Plan must use positional lists');}
        $seen=[];
        foreach($this->names as $name){if(!is_string($name)||!preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/D',$name)||isset($seen[$name])){throw new InvalidArgumentException('Ambiguous name');}$seen[$name]=true;}
        $used=[];
        foreach($this->visible as $index){if(!is_int($index)||!isset($this->names[$index])||isset($used[$index])){throw new InvalidArgumentException('Invalid visible position');}$used[$index]=true;}
        foreach($this->restore as $target=>$entry){
            $shadow=$entry['shadow']??null;$rule=$entry['rule']??null;
            if(!is_int($target)||!in_array($target,$this->visible,true)||!is_int($shadow)||!isset($this->names[$shadow])||isset($used[$shadow])||!is_array($rule)){throw new InvalidArgumentException('Invalid recovery positions');}
            ShadowValue::restore(null,$rule);$used[$shadow]=true;
        }
        if(count($used)!==count($this->names)){throw new InvalidArgumentException('Unaccounted raw column');}
    }
    public function sql(): string {return $this->sql;}
    public function sqlHash(): string {return hash('sha256',$this->sql);}
    public function visibleCount(): int {return count($this->visible);}
    public function rawIndex($index): int {if(!is_int($index)||!array_key_exists($index,$this->visible)){throw new InvalidArgumentException('Invalid visible column');}return $this->visible[$index];}
    public function validateLayout(array $metadata): void
    {
        if(count($metadata)!==count($this->names)){throw new UnexpectedValueException('Column count mismatch');}
        foreach($this->names as $i=>$name){if(($metadata[$i]['name']??null)!==$name){throw new UnexpectedValueException('Column name mismatch');}}
        foreach($this->restore as $entry){$m=$metadata[$entry['shadow']];if(($m['native_type']??null)!=='varchar'||($m['precision']??null)!==128){throw new UnexpectedValueException('Trusted text projection metadata mismatch');}}
    }
    public function restore(array $raw): array
    {
        if(count($raw)!==count($this->names)||array_keys($raw)!==range(0,count($raw)-1)){throw new UnexpectedValueException('Raw layout mismatch');}
        foreach($this->restore as $target=>$entry){
            $text=$raw[$entry['shadow']];
            if(($raw[$target]===null)!==($text===null)){throw new UnexpectedValueException('Null pairing mismatch');}
            $raw[$target]=ShadowValue::restore($text,$entry['rule']);
        }
        $out=[];foreach($this->visible as $position){$out[]=$raw[$position];}return $out;
    }
    public function shape(array $visible,int $mode)
    {
        if(count($visible)!==$this->visibleCount()){throw new UnexpectedValueException('Visible count mismatch');}
        if($mode===PDO::FETCH_NUM){return $visible;}
        if(!in_array($mode,[PDO::FETCH_ASSOC,PDO::FETCH_BOTH,PDO::FETCH_OBJ],true)){throw new InvalidArgumentException('Unsupported row mode');}
        $out=[];foreach($this->visible as $i=>$rawIndex){if($mode===PDO::FETCH_BOTH){$out[$i]=$visible[$i];}$out[$this->names[$rawIndex]]=$visible[$i];}
        return $mode===PDO::FETCH_OBJ?(object)$out:$out;
    }
}

final class ShadowRegistry
{
    private $plans=[];
    public function __construct(array $plans)
    {foreach($plans as $plan){if(!$plan instanceof ShadowPlan||isset($this->plans[$plan->sqlHash()])){throw new InvalidArgumentException('Invalid/duplicate registry entry');}$this->plans[$plan->sqlHash()]=$plan;}}
    public function find(string $sql): ?ShadowPlan
    {$plan=$this->plans[hash('sha256',$sql)]??null;return $plan!==null&&$plan->sql()===$sql?$plan:null;}
}
