<?php
declare(strict_types=1);

namespace App\Services;

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

final class StudioWorkflowService
{
    private const NODE_TYPES = ['start','end','action','decision','approval','notification','assignment','pdf','signature'];

    public function modules(): array
    {
        $q=Database::connection()->query("SELECT id,code,name,version,status 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=:id AND archived_at IS NULL ORDER BY position,id");
        $q->execute(['id'=>$moduleId]); return $q->fetchAll(PDO::FETCH_ASSOC) ?: [];
    }

    public function workflows(int $moduleId): array
    {
        $q=Database::connection()->prepare("SELECT d.*,e.name entity_name,(SELECT COUNT(*) FROM axiom_workflow_nodes n WHERE n.workflow_id=d.id AND n.archived_at IS NULL) node_count FROM axiom_workflow_definitions d LEFT JOIN axiom_studio_entities e ON e.id=d.entity_id WHERE d.module_id=:id AND d.archived_at IS NULL ORDER BY d.updated_at DESC,d.id DESC");
        $q->execute(['id'=>$moduleId]); return $q->fetchAll(PDO::FETCH_ASSOC) ?: [];
    }

    public function workflow(int $id): ?array
    {
        $q=Database::connection()->prepare("SELECT * FROM axiom_workflow_definitions WHERE id=:id AND archived_at IS NULL");
        $q->execute(['id'=>$id]); $r=$q->fetch(PDO::FETCH_ASSOC); return $r?:null;
    }

    public function nodes(int $workflowId): array
    {
        $q=Database::connection()->prepare("SELECT * FROM axiom_workflow_nodes WHERE workflow_id=:id AND archived_at IS NULL ORDER BY position_y,position_x,id");
        $q->execute(['id'=>$workflowId]); return $q->fetchAll(PDO::FETCH_ASSOC) ?: [];
    }

    public function transitions(int $workflowId): array
    {
        $q=Database::connection()->prepare("SELECT t.*,a.label source_label,b.label target_label FROM axiom_workflow_transitions t JOIN axiom_workflow_nodes a ON a.id=t.source_node_id JOIN axiom_workflow_nodes b ON b.id=t.target_node_id WHERE t.workflow_id=:id AND t.archived_at IS NULL ORDER BY t.position,id");
        $q->execute(['id'=>$workflowId]); return $q->fetchAll(PDO::FETCH_ASSOC) ?: [];
    }

    public function nodeTypes(): array
    {
        return [
            'start'=>'Début','end'=>'Fin','action'=>'Action','decision'=>'Décision','approval'=>'Validation',
            'notification'=>'Notification','assignment'=>'Affectation','pdf'=>'Génération PDF','signature'=>'Signature',
        ];
    }

    public function createWorkflow(int $moduleId,array $data,?int $userId): int
    {
        $name=trim((string)($data['name']??'')); $code=$this->code((string)($data['code']??$name));
        if($moduleId<=0 || $name==='') throw new InvalidArgumentException('Le module et le nom du workflow sont obligatoires.');
        $entityId=(int)($data['entity_id']??0) ?: null;
        $pdo=Database::connection();
        $q=$pdo->prepare("INSERT INTO axiom_workflow_definitions(module_id,entity_id,code,name,description,status,current_version,created_by,updated_by) VALUES(:m,:e,:c,:n,:d,'draft',1,:u,:u)");
        $q->execute(['m'=>$moduleId,'e'=>$entityId,'c'=>$code,'n'=>$name,'d'=>trim((string)($data['description']??''))?:null,'u'=>$userId]);
        $id=(int)$pdo->lastInsertId();
        $this->addNode($id,['node_type'=>'start','label'=>'Début','code'=>'start','position_x'=>80,'position_y'=>80],$userId);
        $this->addNode($id,['node_type'=>'end','label'=>'Fin','code'=>'end','position_x'=>520,'position_y'=>80],$userId);
        return $id;
    }

    public function addNode(int $workflowId,array $data,?int $userId): int
    {
        if(!$this->workflow($workflowId)) throw new InvalidArgumentException('Workflow introuvable.');
        $type=(string)($data['node_type']??'action'); if(!in_array($type,self::NODE_TYPES,true)) throw new InvalidArgumentException('Type de nœud non reconnu.');
        $label=trim((string)($data['label']??'')); if($label==='') throw new InvalidArgumentException('Le libellé du nœud est obligatoire.');
        $code=$this->code((string)($data['code']??$label));
        $q=Database::connection()->prepare("INSERT INTO axiom_workflow_nodes(workflow_id,code,label,node_type,configuration_json,position_x,position_y,created_by,updated_by) VALUES(:w,:c,:l,:t,:j,:x,:y,:u,:u)");
        $config=['assignee_type'=>$data['assignee_type']??null,'assignee_value'=>trim((string)($data['assignee_value']??''))?:null,'message'=>trim((string)($data['message']??''))?:null];
        $q->execute(['w'=>$workflowId,'c'=>$code,'l'=>$label,'t'=>$type,'j'=>json_encode($config,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),'x'=>(int)($data['position_x']??240),'y'=>(int)($data['position_y']??160),'u'=>$userId]);
        $this->touch($workflowId,$userId); return (int)Database::connection()->lastInsertId();
    }

    public function addTransition(int $workflowId,array $data,?int $userId): int
    {
        $source=(int)($data['source_node_id']??0); $target=(int)($data['target_node_id']??0);
        if($source<=0 || $target<=0 || $source===$target) throw new InvalidArgumentException('Sélectionnez deux étapes différentes.');
        $nodes=$this->nodes($workflowId); $ids=array_map(static fn(array $n):int=>(int)$n['id'],$nodes);
        if(!in_array($source,$ids,true)||!in_array($target,$ids,true)) throw new InvalidArgumentException('Les étapes ne correspondent pas au workflow.');
        $label=trim((string)($data['label']??'')) ?: 'Continuer';
        $q=Database::connection()->prepare("INSERT INTO axiom_workflow_transitions(workflow_id,source_node_id,target_node_id,label,condition_expression,position,created_by,updated_by) VALUES(:w,:s,:t,:l,:c,:p,:u,:u)");
        $q->execute(['w'=>$workflowId,'s'=>$source,'t'=>$target,'l'=>$label,'c'=>trim((string)($data['condition_expression']??''))?:null,'p'=>(int)($data['position']??0),'u'=>$userId]);
        $this->touch($workflowId,$userId); return (int)Database::connection()->lastInsertId();
    }

    public function publish(int $workflowId,?int $userId): void
    {
        $report=$this->simulate($workflowId); if(!$report['valid']) throw new RuntimeException('Publication impossible : '.implode(' ', $report['errors']));
        $pdo=Database::connection(); $pdo->beginTransaction();
        try {
            $wf=$this->workflow($workflowId); $version=(int)($wf['current_version']??1);
            $snapshot=['workflow'=>$wf,'nodes'=>$this->nodes($workflowId),'transitions'=>$this->transitions($workflowId)];
            $q=$pdo->prepare("INSERT INTO axiom_workflow_versions(workflow_id,version_number,snapshot_json,published_by,published_at) VALUES(:w,: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(['w'=>$workflowId,'v'=>$version,'j'=>json_encode($snapshot,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),'u'=>$userId]);
            $pdo->prepare("UPDATE axiom_workflow_definitions SET status='published',published_at=CURRENT_TIMESTAMP,updated_by=:u,updated_at=CURRENT_TIMESTAMP WHERE id=:id")->execute(['u'=>$userId,'id'=>$workflowId]);
            $pdo->commit();
        } catch (\Throwable $e) { if($pdo->inTransaction())$pdo->rollBack(); throw $e; }
    }

    public function archive(string $type,int $id,?int $userId): void
    {
        $map=['workflow'=>'axiom_workflow_definitions','node'=>'axiom_workflow_nodes','transition'=>'axiom_workflow_transitions'];
        if(!isset($map[$type])||$id<=0) 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 $workflowId): array
    {
        $nodes=$this->nodes($workflowId); $transitions=$this->transitions($workflowId); $errors=[]; $warnings=[];
        $starts=array_values(array_filter($nodes,static fn(array $n):bool=>$n['node_type']==='start'));
        $ends=array_values(array_filter($nodes,static fn(array $n):bool=>$n['node_type']==='end'));
        if(count($starts)!==1)$errors[]='Le workflow doit comporter exactement une étape Début.';
        if(!$ends)$errors[]='Le workflow doit comporter au moins une étape Fin.';
        $adj=[]; foreach($transitions as $t)$adj[(int)$t['source_node_id']][]=(int)$t['target_node_id'];
        $visited=[]; if(count($starts)===1){$stack=[(int)$starts[0]['id']]; while($stack){$id=array_pop($stack);if(isset($visited[$id]))continue;$visited[$id]=true;foreach($adj[$id]??[] as $next)$stack[]=$next;}}
        foreach($nodes as $n){if(!isset($visited[(int)$n['id']]))$warnings[]='Étape non atteignable : '.$n['label'].'.'; if($n['node_type']!=='end' && empty($adj[(int)$n['id']]))$warnings[]='Aucune sortie depuis : '.$n['label'].'.';}
        return ['valid'=>!$errors,'errors'=>$errors,'warnings'=>array_values(array_unique($warnings)),'node_count'=>count($nodes),'transition_count'=>count($transitions),'reachable_count'=>count($visited)];
    }

    private function touch(int $workflowId,?int $userId): void
    {
        Database::connection()->prepare("UPDATE axiom_workflow_definitions SET status='draft',updated_by=:u,updated_at=CURRENT_TIMESTAMP WHERE id=:id")->execute(['u'=>$userId,'id'=>$workflowId]);
    }

    private function code(string $value): string
    {
        $value=strtolower(trim($value)); $value=preg_replace('/[^a-z0-9]+/','_',$value)??''; $value=trim($value,'_');
        if($value===''||preg_match('/^[0-9]/',$value))$value='workflow_'.$value; return substr($value,0,100);
    }
}
