<?php
/** Central print fleet: self-registering Windows nodes, approved printers and shared routes. */
require_once __DIR__ . '/output_devices_lib.php';
require_once __DIR__ . '/install_link_lib.php';

if (!function_exists('ua_output_fleet_migrate')) {
  function ua_output_fleet_migrate(mysqli $conn): void {
    ua_output_ensure_schema($conn);ua_link_ensure_schema($conn);
    $conn->query("CREATE TABLE IF NOT EXISTS ua_output_nodes (
      node_id VARCHAR(100) PRIMARY KEY,instance_id VARCHAR(80) NULL,instance_type VARCHAR(20) NOT NULL,
      instance_name VARCHAR(160) NOT NULL,machine_name VARCHAR(160) NOT NULL,agent_url VARCHAR(220) NOT NULL,
      agent_token VARCHAR(160) NOT NULL,agent_version VARCHAR(30) NOT NULL DEFAULT '',network_enabled TINYINT(1) NOT NULL DEFAULT 0,
      approval_status VARCHAR(20) NOT NULL DEFAULT 'PENDING',is_enabled TINYINT(1) NOT NULL DEFAULT 1,
      remote_addr VARCHAR(80) NULL,last_error VARCHAR(255) NULL,last_seen_at DATETIME NOT NULL,created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
      updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
      KEY idx_output_node_status(approval_status,is_enabled,last_seen_at),KEY idx_output_node_instance(instance_id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
    $conn->query("CREATE TABLE IF NOT EXISTS ua_output_fleet_printers (
      id BIGINT AUTO_INCREMENT PRIMARY KEY,node_id VARCHAR(100) NOT NULL,printer_name VARCHAR(255) NOT NULL,
      driver_name VARCHAR(255) NULL,port_name VARCHAR(160) NULL,is_default TINYINT(1) NOT NULL DEFAULT 0,
      approval_status VARCHAR(20) NOT NULL DEFAULT 'PENDING',share_mode VARCHAR(20) NOT NULL DEFAULT 'NONE',
      is_online TINYINT(1) NOT NULL DEFAULT 1,last_seen_at DATETIME NOT NULL,approved_by INT NULL,approved_at DATETIME NULL,
      created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
      UNIQUE KEY uq_fleet_node_printer(node_id,printer_name),KEY idx_fleet_printer_share(approval_status,share_mode,is_online)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
    $conn->query("CREATE TABLE IF NOT EXISTS ua_output_fleet_access (
      id BIGINT AUTO_INCREMENT PRIMARY KEY,printer_id BIGINT NOT NULL,consumer_scope VARCHAR(160) NOT NULL,
      is_allowed TINYINT(1) NOT NULL DEFAULT 1,created_by INT NULL,created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
      UNIQUE KEY uq_fleet_access(printer_id,consumer_scope),KEY idx_fleet_access_scope(consumer_scope,is_allowed)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
    $conn->query("CREATE TABLE IF NOT EXISTS ua_output_fleet_routes (
      consumer_scope VARCHAR(160) NOT NULL,command_key VARCHAR(80) NOT NULL,printer_id BIGINT NOT NULL,
      paper_size VARCHAR(30) NOT NULL DEFAULT '',copies SMALLINT UNSIGNED NOT NULL DEFAULT 1,is_enabled TINYINT(1) NOT NULL DEFAULT 1,
      updated_by INT NULL,updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
      PRIMARY KEY(consumer_scope,command_key),KEY idx_fleet_route_printer(printer_id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
    $conn->query("CREATE TABLE IF NOT EXISTS ua_output_fleet_jobs (
      id BIGINT AUTO_INCREMENT PRIMARY KEY,node_id VARCHAR(100) NOT NULL,printer_id BIGINT NOT NULL,command_key VARCHAR(80) NOT NULL,
      consumer_scope VARCHAR(160) NOT NULL,title VARCHAR(180) NULL,paper_size VARCHAR(30) NULL,copies SMALLINT UNSIGNED NOT NULL DEFAULT 1,
      payload_html LONGTEXT NOT NULL,job_status VARCHAR(20) NOT NULL DEFAULT 'QUEUED',attempts SMALLINT UNSIGNED NOT NULL DEFAULT 0,
      claimed_at DATETIME NULL,completed_at DATETIME NULL,status_message VARCHAR(500) NULL,created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
      KEY idx_fleet_job_pull(node_id,job_status,created_at),KEY idx_fleet_job_scope(consumer_scope,created_at)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
    foreach(['payload_type'=>"VARCHAR(12) NOT NULL DEFAULT 'HTML' AFTER copies",'file_name'=>"VARCHAR(255) NULL AFTER payload_html",'payload_file'=>"LONGTEXT NULL AFTER file_name"] as $column=>$definition){$check=$conn->query("SHOW COLUMNS FROM ua_output_fleet_jobs LIKE '{$column}'");if(!$check||$check->num_rows===0)$conn->query("ALTER TABLE ua_output_fleet_jobs ADD {$column} {$definition}");}
    $sharedColumn=$conn->query("SHOW COLUMNS FROM ua_output_routes LIKE 'shared_printer_id'");
    if(!$sharedColumn||$sharedColumn->num_rows===0)@$conn->query("ALTER TABLE ua_output_routes ADD shared_printer_id BIGINT NULL AFTER device_name");
  }
}

if (!function_exists('ua_output_fleet_remote_ip')) {function ua_output_fleet_remote_ip(): string{return substr(trim((string)($_SERVER['REMOTE_ADDR']??'')),0,80);}}
if (!function_exists('ua_output_fleet_private_ip')) {function ua_output_fleet_private_ip(string $ip): bool{return filter_var($ip,FILTER_VALIDATE_IP)&&!filter_var($ip,FILTER_VALIDATE_IP,FILTER_FLAG_NO_PRIV_RANGE|FILTER_FLAG_NO_RES_RANGE);}}

if (!function_exists('ua_output_fleet_authenticate')) {
  function ua_output_fleet_authenticate(mysqli $conn,string $raw): array {
    $instance=preg_replace('/[^a-zA-Z0-9_-]/','',(string)($_SERVER['HTTP_X_UA_INSTANCE']??''))??'';
    $ts=(int)($_SERVER['HTTP_X_UA_TIMESTAMP']??0);$nonce=substr(trim((string)($_SERVER['HTTP_X_UA_NONCE']??'')),0,120);$sig=strtolower(trim((string)($_SERVER['HTTP_X_UA_SIGNATURE']??'')));
    $remote=ua_output_fleet_remote_ip();
    if($instance==='server-local' && in_array($remote,['127.0.0.1','::1'],true))return ['ok'=>true,'instance_id'=>'server-local','type'=>'server','name'=>'النسخة الرئيسية'];
    if($instance===''||abs(time()-$ts)>300||$nonce===''||!preg_match('/^[a-f0-9]{64}$/',$sig))return ['ok'=>false,'error'=>'INVALID_AUTH'];
    $esc=$conn->real_escape_string($instance);$rs=$conn->query("SELECT * FROM ua_link_requests WHERE instance_id='{$esc}' AND status='approved' LIMIT 1");$link=$rs&&$rs->num_rows?$rs->fetch_assoc():null;if(!$link)return ['ok'=>false,'error'=>'INSTANCE_NOT_APPROVED'];
    $expected=hash_hmac('sha256',$ts.'.'.$nonce.'.'.$raw,(string)$link['request_secret_hash']);if(!hash_equals($expected,$sig))return ['ok'=>false,'error'=>'INVALID_SIGNATURE'];
    return ['ok'=>true,'instance_id'=>$instance,'type'=>(string)$link['install_type'],'name'=>(string)$link['instance_name']];
  }
}

if (!function_exists('ua_output_fleet_register')) {
  function ua_output_fleet_register(mysqli $conn,array $auth,array $data): array {
    ua_output_fleet_migrate($conn);$nodeId=substr(preg_replace('/[^a-zA-Z0-9_.-]/','-',trim((string)($data['node_id']??'')))??'',0,100);
    if($nodeId==='')return ['ok'=>false,'error'=>'NODE_ID_REQUIRED'];$machine=substr(trim((string)($data['machine_name']??'')),0,160);$token=substr(trim((string)($data['agent_token']??'')),0,160);if(strlen($token)<24)return ['ok'=>false,'error'=>'AGENT_TOKEN_REQUIRED'];
    $remote=ua_output_fleet_remote_ip();$reported=trim((string)($data['agent_url']??''));$port=max(1,min(65535,(int)($data['port']??9123)));$host=$remote;
    if(ua_output_fleet_private_ip($reported))$host=$reported;$url='http://'.(filter_var($host,FILTER_VALIDATE_IP,FILTER_FLAG_IPV6)?'['.$host.']':$host).':'.$port;
    $instance=(string)$auth['instance_id'];$type=(string)$auth['type'];$name=substr((string)($auth['name']?:$machine),0,160);$version=substr((string)($data['version']??''),0,30);$network=!empty($data['network'])?1:0;
    $stmt=$conn->prepare("INSERT INTO ua_output_nodes(node_id,instance_id,instance_type,instance_name,machine_name,agent_url,agent_token,agent_version,network_enabled,remote_addr,last_seen_at) VALUES(?,?,?,?,?,?,?,?,?,?,NOW()) ON DUPLICATE KEY UPDATE instance_id=VALUES(instance_id),instance_type=VALUES(instance_type),instance_name=VALUES(instance_name),machine_name=VALUES(machine_name),agent_url=VALUES(agent_url),agent_token=VALUES(agent_token),agent_version=VALUES(agent_version),network_enabled=VALUES(network_enabled),remote_addr=VALUES(remote_addr),last_seen_at=NOW(),last_error=NULL");
    $stmt->bind_param('ssssssssis',$nodeId,$instance,$type,$name,$machine,$url,$token,$version,$network,$remote);$stmt->execute();$stmt->close();
    $seen=[];foreach((array)($data['printers']??[]) as $item){$printer=is_array($item)?trim((string)($item['name']??'')):trim((string)$item);if($printer==='')continue;$printer=mb_substr($printer,0,255);$seen[]=$printer;$driver=is_array($item)?mb_substr(trim((string)($item['driver']??'')),0,255):'';$portName=is_array($item)?mb_substr(trim((string)($item['port']??'')),0,160):'';$default=is_array($item)&&!empty($item['default'])?1:0;
      $stmt=$conn->prepare("INSERT INTO ua_output_fleet_printers(node_id,printer_name,driver_name,port_name,is_default,last_seen_at) VALUES(?,?,?,?,?,NOW()) ON DUPLICATE KEY UPDATE driver_name=VALUES(driver_name),port_name=VALUES(port_name),is_default=VALUES(is_default),is_online=1,last_seen_at=NOW()");$stmt->bind_param('ssssi',$nodeId,$printer,$driver,$portName,$default);$stmt->execute();$stmt->close();}
    $nodeEsc=$conn->real_escape_string($nodeId);if($seen){$quoted=array_map(fn($v)=>"'".$conn->real_escape_string($v)."'",$seen);$conn->query("UPDATE ua_output_fleet_printers SET is_online=0 WHERE node_id='{$nodeEsc}' AND printer_name NOT IN (".implode(',',$quoted).")");}else $conn->query("UPDATE ua_output_fleet_printers SET is_online=0 WHERE node_id='{$nodeEsc}'");
    return ['ok'=>true,'node_id'=>$nodeId,'approval_status'=>(string)(($conn->query("SELECT approval_status FROM ua_output_nodes WHERE node_id='{$nodeEsc}'")->fetch_assoc())['approval_status']??'PENDING'),'registered_printers'=>count($seen)];
  }
}

if (!function_exists('ua_output_fleet_printers')) {
  function ua_output_fleet_printers(mysqli $conn,bool $approvedOnly=false): array {ua_output_fleet_migrate($conn);$where=$approvedOnly?"WHERE n.approval_status='APPROVED' AND n.is_enabled=1 AND p.approval_status='APPROVED' AND p.share_mode<>'NONE'":"";$rows=[];$rs=$conn->query("SELECT p.*,n.instance_name,n.instance_type,n.machine_name,n.agent_url,n.last_seen_at node_last_seen FROM ua_output_fleet_printers p JOIN ua_output_nodes n ON n.node_id=p.node_id {$where} ORDER BY n.instance_name,p.printer_name");while($rs&&($r=$rs->fetch_assoc()))$rows[]=$r;return $rows;}
}

if (!function_exists('ua_output_fleet_consumer_scopes')) {
  function ua_output_fleet_consumer_scopes(mysqli $conn): array {$scopes=['ALL'=>'جميع النسخ المصرح بها',ua_install_instance_key()=>'النسخة الرئيسية'];$rs=$conn->query("SELECT instance_id,instance_name,install_type FROM ua_link_requests WHERE status='approved' ORDER BY instance_name");while($rs&&($r=$rs->fetch_assoc()))$scopes['linked:'.$r['instance_id']]=$r['instance_name'].' — '.($r['install_type']==='branch'?'فرعية':'مندوب');return $scopes;}
}

if (!function_exists('ua_output_fleet_scope_allowed')) {
  function ua_output_fleet_scope_allowed(mysqli $conn,array $printer,string $scope): bool {$scope=ua_output_scope_key($scope);if((string)$printer['share_mode']==='ALL')return true;if((string)$printer['share_mode']!=='SELECTED')return false;$id=(int)$printer['id'];$esc=$conn->real_escape_string($scope);$base=preg_replace('/\:device\:.+$/','',$scope);$baseEsc=$conn->real_escape_string((string)$base);$rs=$conn->query("SELECT id FROM ua_output_fleet_access WHERE printer_id={$id} AND is_allowed=1 AND consumer_scope IN ('{$esc}','{$baseEsc}') LIMIT 1");return $rs&&$rs->num_rows>0;}
}

if (!function_exists('ua_output_fleet_resolve')) {
  function ua_output_fleet_resolve(mysqli $conn,string $command,string $scope): array {ua_output_fleet_migrate($conn);$scope=ua_output_scope_key($scope);$base=(string)(preg_replace('/\:device\:.+$/','',$scope)??$scope);$scopes=[$scope,$base,'ALL'];foreach(array_unique($scopes) as $candidate){$esc=$conn->real_escape_string($candidate);$cmd=$conn->real_escape_string($command);$rs=$conn->query("SELECT r.*,p.node_id,p.printer_name,p.share_mode,p.approval_status printer_status,p.is_online,n.agent_url,n.agent_token,n.network_enabled,n.approval_status node_status,n.is_enabled node_enabled,n.last_seen_at node_last_seen FROM ua_output_fleet_routes r JOIN ua_output_fleet_printers p ON p.id=r.printer_id JOIN ua_output_nodes n ON n.node_id=p.node_id WHERE r.consumer_scope='{$esc}' AND r.command_key='{$cmd}' AND r.is_enabled=1 LIMIT 1");if($rs&&($row=$rs->fetch_assoc())){if($row['node_status']==='APPROVED'&&(int)$row['node_enabled']===1&&$row['printer_status']==='APPROVED'&&ua_output_fleet_scope_allowed($conn,$row,$scope))return $row;}}return [];}
}

if (!function_exists('ua_output_fleet_dispatch')) {
  function ua_output_fleet_dispatch(mysqli $conn,string $command,string $scope,array $payload): array {$route=ua_output_fleet_resolve($conn,$command,$scope);if(!$route)return ['ok'=>false,'status'=>422,'error'=>'SHARED_PRINTER_NOT_AUTHORIZED'];$agentPayload=json_encode(['printer'=>$route['printer_name'],'copies'=>max(1,(int)($route['copies']??1)),'paper'=>(string)($route['paper_size']??''),'title'=>(string)($payload['title']??$command),'html'=>(string)($payload['html']??'')],JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);$result=!empty($route['network_enabled'])?ua_output_agent_request((string)$route['agent_url'],(string)$route['agent_token'],'print','POST',$agentPayload,45):['ok'=>false,'status'=>0,'body'=>'','error'=>'reverse_channel_required'];if(!empty($result['ok']))return $result+['printer'=>(string)$route['printer_name'],'route'=>$route,'delivery'=>'direct'];
    // No inbound firewall/network setup is required: queue on the main server
    // and let the host node pull through its already-established outbound link.
    $nodeId=(string)$route['node_id'];$printerId=(int)$route['printer_id'];$paper=(string)($route['paper_size']??'');$copies=max(1,(int)($route['copies']??1));$title=mb_substr((string)($payload['title']??$command),0,180);$html=(string)($payload['html']??'');$scope=ua_output_scope_key($scope);
    $stmt=$conn->prepare("INSERT INTO ua_output_fleet_jobs(node_id,printer_id,command_key,consumer_scope,title,paper_size,copies,payload_html) VALUES(?,?,?,?,?,?,?,?)");$stmt->bind_param('sissssis',$nodeId,$printerId,$command,$scope,$title,$paper,$copies,$html);if(!$stmt->execute()){ $error=$stmt->error;$stmt->close();return ['ok'=>false,'status'=>500,'error'=>$error];}$jobId=(int)$stmt->insert_id;$stmt->close();return ['ok'=>true,'status'=>202,'body'=>json_encode(['ok'=>true,'queued'=>true,'job_id'=>$jobId,'printer'=>$route['printer_name']],JSON_UNESCAPED_UNICODE),'data'=>['job_id'=>$jobId],'error'=>'','printer'=>(string)$route['printer_name'],'route'=>$route,'delivery'=>'pull_queue'];}
}

if (!function_exists('ua_output_fleet_pull')) {
  function ua_output_fleet_pull(mysqli $conn,string $nodeId): array {ua_output_fleet_migrate($conn);$nodeId=substr($nodeId,0,100);$esc=$conn->real_escape_string($nodeId);$conn->begin_transaction();try{$rs=$conn->query("SELECT j.*,p.printer_name FROM ua_output_fleet_jobs j JOIN ua_output_fleet_printers p ON p.id=j.printer_id WHERE j.node_id='{$esc}' AND j.job_status='QUEUED' ORDER BY j.id LIMIT 1 FOR UPDATE");$job=$rs&&$rs->num_rows?$rs->fetch_assoc():null;if(!$job){$conn->commit();return ['ok'=>true,'job'=>null];}$id=(int)$job['id'];$conn->query("UPDATE ua_output_fleet_jobs SET job_status='CLAIMED',claimed_at=NOW(),attempts=attempts+1 WHERE id={$id}");$conn->commit();return ['ok'=>true,'job'=>['id'=>$id,'type'=>$job['payload_type']?:'HTML','printer'=>$job['printer_name'],'copies'=>(int)$job['copies'],'paper'=>$job['paper_size'],'title'=>$job['title'],'html'=>$job['payload_html'],'file_name'=>$job['file_name'],'file_base64'=>$job['payload_file']]];}catch(Throwable $e){$conn->rollback();return ['ok'=>false,'error'=>$e->getMessage()];}}
}

if (!function_exists('ua_output_fleet_dispatch_file')) {
  function ua_output_fleet_dispatch_file(mysqli $conn,string $command,string $scope,array $payload): array {$route=ua_output_fleet_resolve($conn,$command,$scope);if(!$route)return ['ok'=>false,'status'=>422,'error'=>'SHARED_PRINTER_NOT_AUTHORIZED'];$file=(string)($payload['file_base64']??'');$name=mb_substr((string)($payload['file_name']??'document.pdf'),0,255);if($file===''||strlen($file)>45*1024*1024)return ['ok'=>false,'status'=>413,'error'=>'INVALID_PRINT_FILE'];$agentPayload=json_encode(['printer'=>$route['printer_name'],'copies'=>max(1,(int)$route['copies']),'file_name'=>$name,'file_base64'=>$file],JSON_UNESCAPED_SLASHES);$direct=ua_output_agent_request((string)$route['agent_url'],(string)$route['agent_token'],'print-file','POST',$agentPayload,60);if(!empty($direct['ok']))return $direct+['printer'=>$route['printer_name'],'delivery'=>'direct'];$node=(string)$route['node_id'];$pid=(int)$route['printer_id'];$copies=max(1,(int)$route['copies']);$scope=ua_output_scope_key($scope);$type='FILE';$empty='';$stmt=$conn->prepare("INSERT INTO ua_output_fleet_jobs(node_id,printer_id,command_key,consumer_scope,title,paper_size,copies,payload_type,payload_html,file_name,payload_file) VALUES(?,?,?,?,?,'',?,?,?,?,?)");$title=$name;$stmt->bind_param('sisssissss',$node,$pid,$command,$scope,$title,$copies,$type,$empty,$name,$file);if(!$stmt->execute()){$error=$stmt->error;$stmt->close();return ['ok'=>false,'status'=>500,'error'=>$error];}$id=(int)$stmt->insert_id;$stmt->close();return ['ok'=>true,'status'=>202,'body'=>json_encode(['ok'=>true,'queued'=>true,'job_id'=>$id,'printer'=>$route['printer_name']],JSON_UNESCAPED_UNICODE),'printer'=>$route['printer_name'],'delivery'=>'pull_queue'];}
}

if (!function_exists('ua_output_fleet_complete')) {
  function ua_output_fleet_complete(mysqli $conn,string $nodeId,int $jobId,bool $success,string $message): array {$node=$conn->real_escape_string(substr($nodeId,0,100));$status=$success?'COMPLETED':'FAILED';$msg=$conn->real_escape_string(substr($message,0,500));$conn->query("UPDATE ua_output_fleet_jobs SET job_status='{$status}',completed_at=NOW(),status_message='{$msg}' WHERE id=".(int)$jobId." AND node_id='{$node}' AND job_status='CLAIMED'");return ['ok'=>$conn->affected_rows===1];}
}

if (!function_exists('ua_output_fleet_central_request')) {
  function ua_output_fleet_central_request(array $payload): array {
    $link=ua_install_link_config();$instance=trim((string)($link['instance_id']??''));$secret=trim((string)($link['secret']??''));$base=rtrim((string)($link['server_url']??ua_install_server_url()),'/');
    if($instance===''||$secret===''||$base==='')return ['ok'=>false,'status'=>0,'error'=>'CENTRAL_LINK_UNAVAILABLE'];
    $body=json_encode($payload,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);$ts=time();$nonce=bin2hex(random_bytes(16));$key=hash('sha256',$secret);$signature=hash_hmac('sha256',$ts.'.'.$nonce.'.'.$body,$key);
    $adminBase=preg_match('~/admin$~i',$base)?$base:rtrim($base,'/').'/mikrotik/admin';$ch=curl_init($adminBase.'/api/output_fleet_print.php');curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_POST=>true,CURLOPT_POSTFIELDS=>$body,CURLOPT_HTTPHEADER=>['Content-Type: application/json','X-UA-Instance: '.$instance,'X-UA-Timestamp: '.$ts,'X-UA-Nonce: '.$nonce,'X-UA-Signature: '.$signature],CURLOPT_CONNECTTIMEOUT=>5,CURLOPT_TIMEOUT=>55]);$raw=curl_exec($ch);$error=curl_error($ch);$status=(int)curl_getinfo($ch,CURLINFO_HTTP_CODE);curl_close($ch);$data=json_decode((string)$raw,true);return ['ok'=>$raw!==false&&$status>=200&&$status<300,'status'=>$status,'body'=>(string)$raw,'data'=>is_array($data)?$data:null,'error'=>$error?:((string)($data['error']??''))];
  }
}
