<?php
declare(strict_types=1);

namespace App\Services;

use App\Core\Database;
use InvalidArgumentException;
use PDO;
use RuntimeException;
use Throwable;

final class StudioRuleService
{
    private const TRIGGERS = [
        'before_create','after_create','before_update','after_update',
        'before_delete','after_delete','validation','scheduled','manual',
    ];
    private const OPERATORS = [
        'equals','not_equals','contains','starts_with','greater_than','less_than',
        'is_empty','is_not_empty','in_list','between',
    ];
    private const ACTIONS = [
        'set_field','create_task','start_workflow','notification','email','api_call','document','audit',
    ];

    public function modules(): array
    {
        $q=Database::connection()->query("SELECT id,code,name FROM axiom_studio_modules WHERE archived_at IS NULL ORDER BY name");
        return $q->fetchAll(PDO::FETCH_ASSOC) ?: [];
    }

    public function entities(int $moduleId): array
    {
        $q=Database::connection()->prepare("SELECT id,name,code,table_name FROM axiom_studio_entities WHERE module_id=:m AND archived_at IS NULL ORDER BY position,id");
        $q->execute(['m'=>$moduleId]);
        return $q->fetchAll(PDO::FETCH_ASSOC) ?: [];
    }

    public function fields(int $entityId): array
    {
        $q=Database::connection()->prepare("SELECT id,name,code,column_name,field_type FROM axiom_studio_fields WHERE entity_id=:e AND archived_at IS NULL ORDER BY position,id");
        $q->execute(['e'=>$entityId]);
        return $q->fetchAll(PDO::FETCH_ASSOC) ?: [];
    }

    public function rules(int $moduleId): array
    {
        $q=Database::connection()->prepare("SELECT r.*,e.name entity_name,(SELECT COUNT(*) FROM axiom_rule_conditions c WHERE c.rule_id=r.id AND c.archived_at IS NULL) condition_count,(SELECT COUNT(*) FROM axiom_rule_actions a WHERE a.rule_id=r.id AND a.archived_at IS NULL) action_count FROM axiom_rules r LEFT JOIN axiom_studio_entities e ON e.id=r.entity_id WHERE r.module_id=:m AND r.archived_at IS NULL ORDER BY r.updated_at DESC,r.id DESC");
        $q->execute(['m'=>$moduleId]);
        return $q->fetchAll(PDO::FETCH_ASSOC) ?: [];
    }

    public function rule(int $id): ?array
    {
        $q=Database::connection()->prepare("SELECT r.*,e.name entity_name,e.table_name FROM axiom_rules r LEFT JOIN axiom_studio_entities e ON e.id=r.entity_id WHERE r.id=:id AND r.archived_at IS NULL");
        $q->execute(['id'=>$id]);
        $row=$q->fetch(PDO::FETCH_ASSOC);
        return $row ?: null;
    }

    public function conditions(int $ruleId): array
    {
        $q=Database::connection()->prepare("SELECT c.*,f.name field_name,f.column_name FROM axiom_rule_conditions c LEFT JOIN axiom_studio_fields f ON f.id=c.field_id WHERE c.rule_id=:r AND c.archived_at IS NULL ORDER BY c.position,c.id");
        $q->execute(['r'=>$ruleId]);
        return $q->fetchAll(PDO::FETCH_ASSOC) ?: [];
    }

    public function actions(int $ruleId): array
    {
        $q=Database::connection()->prepare("SELECT * FROM axiom_rule_actions WHERE rule_id=:r AND archived_at IS NULL ORDER BY position,id");
        $q->execute(['r'=>$ruleId]);
        $rows=$q->fetchAll(PDO::FETCH_ASSOC) ?: [];
        foreach($rows as &$row){$row['config']=json_decode((string)($row['configuration_json']??'{}'),true) ?: [];}
        return $rows;
    }

    public function logs(int $ruleId,int $limit=40): array
    {
        $limit=max(1,min(200,$limit));
        $q=Database::connection()->prepare("SELECT * FROM axiom_rule_logs WHERE rule_id=:r ORDER BY id DESC LIMIT {$limit}");
        $q->execute(['r'=>$ruleId]);
        return $q->fetchAll(PDO::FETCH_ASSOC) ?: [];
    }

    public function triggers(): array
    {
        return ['before_create'=>'Avant création','after_create'=>'Après création','before_update'=>'Avant modification','after_update'=>'Après modification','before_delete'=>'Avant suppression','after_delete'=>'Après suppression','validation'=>'Validation','scheduled'=>'Planifiée','manual'=>'Manuelle'];
    }

    public function operators(): array
    {
        return ['equals'=>'Égal à','not_equals'=>'Différent de','contains'=>'Contient','starts_with'=>'Commence par','greater_than'=>'Supérieur à','less_than'=>'Inférieur à','is_empty'=>'Est vide','is_not_empty'=>'N’est pas vide','in_list'=>'Dans la liste','between'=>'Entre deux valeurs'];
    }

    public function actionTypes(): array
    {
        return ['set_field'=>'Modifier un champ','create_task'=>'Créer une tâche','start_workflow'=>'Déclencher un workflow','notification'=>'Créer une notification','email'=>'Envoyer un e-mail','api_call'=>'Appeler une API','document'=>'Générer un document','audit'=>'Écrire dans l’audit'];
    }

    public function saveRule(int $moduleId,array $data,?int $userId): int
    {
        $name=trim((string)($data['name']??''));
        $entityId=(int)($data['entity_id']??0);
        $trigger=(string)($data['trigger_event']??'manual');
        $logic=strtoupper((string)($data['condition_logic']??'AND'))==='OR'?'OR':'AND';
        if($moduleId<1||$entityId<1||$name==='') throw new InvalidArgumentException('Module, entité et nom sont obligatoires.');
        if(!in_array($trigger,self::TRIGGERS,true)) throw new InvalidArgumentException('Déclencheur non reconnu.');
        $id=(int)($data['rule_id']??0);$pdo=Database::connection();$code=$this->code((string)($data['code']??$name));
        if($id>0){
            $q=$pdo->prepare("UPDATE axiom_rules SET entity_id=:e,name=:n,description=:d,trigger_event=:t,condition_logic=:l,priority=:p,stop_on_match=:s,status='draft',updated_by=:u,updated_at=CURRENT_TIMESTAMP WHERE id=:id AND module_id=:m AND archived_at IS NULL");
            $q->execute(['e'=>$entityId,'n'=>$name,'d'=>trim((string)($data['description']??''))?:null,'t'=>$trigger,'l'=>$logic,'p'=>max(0,min(9999,(int)($data['priority']??100))),'s'=>isset($data['stop_on_match'])?1:0,'u'=>$userId,'id'=>$id,'m'=>$moduleId]);
            return $id;
        }
        $code=$this->uniqueCode($code);
        $q=$pdo->prepare("INSERT INTO axiom_rules(module_id,entity_id,code,name,description,trigger_event,condition_logic,priority,stop_on_match,status,current_version,created_by,updated_by) VALUES(:m,:e,:c,:n,:d,:t,:l,:p,:s,'draft',1,:u,:u)");
        $q->execute(['m'=>$moduleId,'e'=>$entityId,'c'=>$code,'n'=>$name,'d'=>trim((string)($data['description']??''))?:null,'t'=>$trigger,'l'=>$logic,'p'=>max(0,min(9999,(int)($data['priority']??100))),'s'=>isset($data['stop_on_match'])?1:0,'u'=>$userId]);
        return (int)$pdo->lastInsertId();
    }

    public function addCondition(int $ruleId,array $data,?int $userId): int
    {
        if(!$this->rule($ruleId)) throw new RuntimeException('Règle introuvable.');
        $fieldId=(int)($data['field_id']??0);$operator=(string)($data['operator']??'equals');
        if($fieldId<1||!in_array($operator,self::OPERATORS,true)) throw new InvalidArgumentException('Champ ou opérateur invalide.');
        $q=Database::connection()->prepare("INSERT INTO axiom_rule_conditions(rule_id,group_number,connector,field_id,operator,value_one,value_two,position,created_by,updated_by) VALUES(:r,:g,:c,:f,:o,:v1,:v2,:p,:u,:u)");
        $q->execute(['r'=>$ruleId,'g'=>max(1,(int)($data['group_number']??1)),'c'=>strtoupper((string)($data['connector']??'AND'))==='OR'?'OR':'AND','f'=>$fieldId,'o'=>$operator,'v1'=>trim((string)($data['value_one']??'')),'v2'=>trim((string)($data['value_two']??'')),'p'=>max(0,(int)($data['position']??0)),'u'=>$userId]);
        $this->touch($ruleId,$userId);return (int)Database::connection()->lastInsertId();
    }

    public function addAction(int $ruleId,array $data,?int $userId): int
    {
        if(!$this->rule($ruleId)) throw new RuntimeException('Règle introuvable.');
        $type=(string)($data['action_type']??'audit');if(!in_array($type,self::ACTIONS,true)) throw new InvalidArgumentException('Action non reconnue.');
        $label=trim((string)($data['label']??'')) ?: ($this->actionTypes()[$type]??$type);
        $config=['target_field'=>trim((string)($data['target_field']??'')),'value'=>trim((string)($data['action_value']??'')),'recipient'=>trim((string)($data['recipient']??'')),'message'=>trim((string)($data['message']??'')),'url'=>trim((string)($data['url']??'')),'workflow_id'=>(int)($data['workflow_id']??0),'document_id'=>(int)($data['document_id']??0)];
        $q=Database::connection()->prepare("INSERT INTO axiom_rule_actions(rule_id,action_type,label,configuration_json,position,continue_on_error,created_by,updated_by) VALUES(:r,:t,:l,:j,:p,:c,:u,:u)");
        $q->execute(['r'=>$ruleId,'t'=>$type,'l'=>$label,'j'=>json_encode($config,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),'p'=>max(0,(int)($data['position']??0)),'c'=>isset($data['continue_on_error'])?1:0,'u'=>$userId]);
        $this->touch($ruleId,$userId);return (int)Database::connection()->lastInsertId();
    }

    public function archiveItem(string $type,int $id,?int $userId): void
    {
        $map=['rule'=>'axiom_rules','condition'=>'axiom_rule_conditions','action'=>'axiom_rule_actions'];
        if(!isset($map[$type])||$id<1) throw new InvalidArgumentException('Élément invalide.');
        $q=Database::connection()->prepare("UPDATE {$map[$type]} SET archived_at=CURRENT_TIMESTAMP,updated_by=:u,updated_at=CURRENT_TIMESTAMP WHERE id=:id");
        $q->execute(['u'=>$userId,'id'=>$id]);
    }

    public function simulate(int $ruleId,array $record): array
    {
        $rule=$this->rule($ruleId);if(!$rule) throw new RuntimeException('Règle introuvable.');
        $evaluations=[];$grouped=[];
        foreach($this->conditions($ruleId) as $condition){
            $field=(string)($condition['column_name']??'');$actual=$record[$field]??null;$matched=$this->evaluate($actual,(string)$condition['operator'],(string)($condition['value_one']??''),(string)($condition['value_two']??''));
            $entry=['condition_id'=>(int)$condition['id'],'field'=>$field,'field_name'=>$condition['field_name']??$field,'operator'=>$condition['operator'],'actual'=>$actual,'expected'=>$condition['value_one'],'expected_two'=>$condition['value_two'],'matched'=>$matched];
            $evaluations[]=$entry;$grouped[(int)$condition['group_number']][]=['matched'=>$matched,'connector'=>$condition['connector']];
        }
        $groups=[];foreach($grouped as $number=>$items){$result=null;foreach($items as $item){$result=$result===null?$item['matched']:($item['connector']==='OR'?($result||$item['matched']):($result&&$item['matched']));}$groups[$number]=(bool)$result;}
        $matched=!$groups?true:(strtoupper((string)$rule['condition_logic'])==='OR'?in_array(true,$groups,true):!in_array(false,$groups,true));
        $actions=array_map(static fn(array $a):array=>['id'=>(int)$a['id'],'type'=>$a['action_type'],'label'=>$a['label'],'config'=>$a['config']],$this->actions($ruleId));
        return ['matched'=>$matched,'rule'=>$rule,'evaluations'=>$evaluations,'groups'=>$groups,'actions'=>$matched?$actions:[],'message'=>$matched?'La règle correspond : les actions seraient exécutées.':'La règle ne correspond pas : aucune action.'];
    }

    public function executeManual(int $ruleId,array $record,?int $userId): array
    {
        $started=microtime(true);$simulation=$this->simulate($ruleId,$record);$status=$simulation['matched']?'matched':'not_matched';$details=['simulation'=>$simulation,'executed'=>[]];
        if($simulation['matched']){
            foreach($simulation['actions'] as $action){
                // V1 : exécution sûre des actions sans effets externes ; les connecteurs seront orchestrés en 9.5.0.
                $details['executed'][]=['action_id'=>$action['id'],'type'=>$action['type'],'status'=>'prepared','message'=>'Action validée et préparée pour l’Automation Engine.'];
            }
        }
        $duration=(int)round((microtime(true)-$started)*1000);
        $q=Database::connection()->prepare("INSERT INTO axiom_rule_logs(rule_id,trigger_event,record_key,matched,status,duration_ms,input_json,result_json,executed_by,created_at) VALUES(:r,'manual',:k,:m,:s,:d,:i,:o,:u,CURRENT_TIMESTAMP)");
        $q->execute(['r'=>$ruleId,'k'=>(string)($record['id']??''),'m'=>$simulation['matched']?1:0,'s'=>$status,'d'=>$duration,'i'=>json_encode($record,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),'o'=>json_encode($details,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),'u'=>$userId]);
        return $details+['duration_ms'=>$duration];
    }

    public function publish(int $ruleId,?int $userId): void
    {
        $rule=$this->rule($ruleId);if(!$rule) throw new RuntimeException('Règle introuvable.');
        if(!$this->actions($ruleId)) throw new RuntimeException('Ajoutez au moins une action avant publication.');
        $pdo=Database::connection();$pdo->beginTransaction();
        try{
            $version=(int)($rule['current_version']??1);
            $snapshot=['rule'=>$rule,'conditions'=>$this->conditions($ruleId),'actions'=>$this->actions($ruleId)];
            $q=$pdo->prepare("INSERT INTO axiom_rule_versions(rule_id,version_number,snapshot_json,published_by,published_at) VALUES(:r,:v,:j,:u,CURRENT_TIMESTAMP) ON DUPLICATE KEY UPDATE snapshot_json=VALUES(snapshot_json),published_by=VALUES(published_by),published_at=CURRENT_TIMESTAMP");
            $q->execute(['r'=>$ruleId,'v'=>$version,'j'=>json_encode($snapshot,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),'u'=>$userId]);
            $pdo->prepare("UPDATE axiom_rules SET status='published',published_at=CURRENT_TIMESTAMP,updated_by=:u,updated_at=CURRENT_TIMESTAMP WHERE id=:id")->execute(['u'=>$userId,'id'=>$ruleId]);
            $pdo->commit();
        }catch(Throwable $e){if($pdo->inTransaction())$pdo->rollBack();throw $e;}
    }

    private function evaluate(mixed $actual,string $operator,string $one,string $two): bool
    {
        $a=is_scalar($actual)?(string)$actual:'';
        return match($operator){
            'equals'=>$a===$one,'not_equals'=>$a!==$one,
            'contains'=>$one===''||mb_stripos($a,$one)!==false,
            'starts_with'=>$one===''||mb_stripos($a,$one)===0,
            'greater_than'=>is_numeric($a)&&is_numeric($one)&&(float)$a>(float)$one,
            'less_than'=>is_numeric($a)&&is_numeric($one)&&(float)$a<(float)$one,
            'is_empty'=>trim($a)==='', 'is_not_empty'=>trim($a)!=='',
            'in_list'=>in_array(trim($a),array_map('trim',explode(',',$one)),true),
            'between'=>is_numeric($a)&&is_numeric($one)&&is_numeric($two)&&(float)$a>=(float)$one&&(float)$a<=(float)$two,
            default=>false,
        };
    }

    private function touch(int $ruleId,?int $userId): void
    {
        Database::connection()->prepare("UPDATE axiom_rules SET status='draft',current_version=current_version+1,updated_by=:u,updated_at=CURRENT_TIMESTAMP WHERE id=:id")->execute(['u'=>$userId,'id'=>$ruleId]);
    }
    private function code(string $v): string{$v=iconv('UTF-8','ASCII//TRANSLIT//IGNORE',$v)?:$v;$v=strtolower(trim(preg_replace('/[^a-zA-Z0-9]+/','_',$v),'_'));return substr($v?:'regle',0,110);}
    private function uniqueCode(string $base): string{$c=$base;$i=2;$q=Database::connection()->prepare("SELECT COUNT(*) FROM axiom_rules WHERE code=:c AND archived_at IS NULL");while(true){$q->execute(['c'=>$c]);if((int)$q->fetchColumn()===0)return $c;$c=$base.'_'.$i++;}}
}
