<?php
declare(strict_types=1);
namespace App\Controllers;
use App\Core\Controller;use App\Core\Request;use App\Services\ProductService;use App\Support\SimplePdf;use PDO;
final class ProductController extends Controller{
 private function service():ProductService{return new ProductService(app()->db());}
 public function index(Request$r):void{$filters=$r->all();$result=$this->service()->paginate($filters,max(1,(int)$r->input('page',1)));$refs=$this->service()->references();$this->view('products/index',compact('filters','result','refs'));}
 public function create(Request$r):void{$refs=$this->service()->references();$this->view('products/form',compact('refs'));}
 public function edit(Request$r):void{$product=$this->service()->find((int)$r->param('id'));$refs=$this->service()->references();$this->view('products/form',compact('product','refs'));}
 public function show(Request$r):void{$product=$this->service()->find((int)$r->param('id'));$related=$this->service()->related((int)$product['id']);$refs=$this->service()->references();$this->view('products/show',compact('product','related','refs'));}
 public function store(Request$r):never{$this->persist($r,null);}
 public function update(Request$r):never{$this->persist($r,(int)$r->param('id'));}
 private function persist(Request$r,?int$id):never{try{$pid=$this->service()->save($r->all(),$id);}catch(\InvalidArgumentException$e){$this->backWithErrors(explode('|',$e->getMessage()),$r->all());}$this->redirect('products/'.$pid,$id?'Article modifié.':'Article créé.');}
 public function status(Request$r):never{$this->service()->status((int)$r->param('id'),(string)$r->input('status'));$this->redirect('products/'.$r->param('id'),'Statut mis à jour.');}
 public function duplicate(Request$r):never{$id=$this->service()->duplicate((int)$r->param('id'));$this->redirect('products/'.$id,'Article dupliqué.');}
 public function addRelated(Request$r):never{$product=$this->service()->find((int)$r->param('id'));$kind=(string)$r->param('kind');$d=$r->all();$pdo=app()->db()->connection();$actor=app()->auth()->user();try{app()->db()->transaction(function(PDO$pdo)use($product,$kind,$d,$actor){if($kind==='packaging'){if((float)($d['contained_quantity']??0)<=0)throw new \InvalidArgumentException('Quantité de conditionnement invalide.');if(!empty($d['is_default']))$pdo->prepare('UPDATE product_packagings SET is_default=0 WHERE product_id=:id')->execute(['id'=>$product['id']]);$pdo->prepare('INSERT INTO product_packagings(product_id,name,unit_id,contained_quantity,barcode,purchase_price,sale_price,is_default)VALUES(:p,:name,:unit,:qty,:barcode,:purchase,:sale,:default)')->execute(['p'=>$product['id'],'name'=>$d['name'],'unit'=>$d['unit_id'],'qty'=>$d['contained_quantity'],'barcode'=>$d['barcode']?:null,'purchase'=>$d['purchase_price']?:null,'sale'=>$d['sale_price']?:null,'default'=>!empty($d['is_default'])?1:0]);}elseif($kind==='supplier'){if(!empty($d['is_primary']))$pdo->prepare('UPDATE product_suppliers SET is_primary=0 WHERE product_id=:id')->execute(['id'=>$product['id']]);$pdo->prepare('INSERT INTO product_suppliers(product_id,supplier_id,supplier_reference,purchase_price,minimum_order_quantity,delivery_days,is_primary,currency_code,notes)VALUES(:p,:supplier,:reference,:price,:minimum,:days,:primary,:currency,:notes)')->execute(['p'=>$product['id'],'supplier'=>$d['supplier_id'],'reference'=>$d['supplier_reference']?:null,'price'=>$d['purchase_price']?:0,'minimum'=>$d['minimum_order_quantity']?:1,'days'=>$d['delivery_days']?:0,'primary'=>!empty($d['is_primary'])?1:0,'currency'=>$d['currency_code']?:'MAD','notes'=>$d['notes']?:null]);}elseif($kind==='component'){if((int)$d['component_product_id']===(int)$product['id']||(float)$d['quantity']<=0)throw new \InvalidArgumentException('Composant invalide.');$pdo->prepare('INSERT INTO product_components(product_id,component_product_id,quantity)VALUES(:p,:component,:qty)')->execute(['p'=>$product['id'],'component'=>$d['component_product_id'],'qty'=>$d['quantity']]);}elseif($kind==='quantity-price'){$min=(float)$d['minimum_quantity'];$max=$d['maximum_quantity']===''?null:(float)$d['maximum_quantity'];if($min<=0||($max!==null&&$max<$min)||(float)$d['unit_price']<0)throw new \InvalidArgumentException('Tranche invalide.');$s=$pdo->prepare('SELECT COUNT(*) FROM quantity_prices WHERE product_id=:p AND COALESCE(price_level_id,0)=:level AND minimum_quantity<=COALESCE(:maximum,999999999) AND COALESCE(maximum_quantity,999999999)>=:minimum');$s->execute(['p'=>$product['id'],'level'=>(int)($d['price_level_id']??0),'maximum'=>$max,'minimum'=>$min]);if($s->fetchColumn())throw new \InvalidArgumentException('Cette tranche chevauche un tarif existant.');$pdo->prepare('INSERT INTO quantity_prices(product_id,price_level_id,minimum_quantity,maximum_quantity,unit_price,valid_from,valid_to)VALUES(:p,:level,:minimum,:maximum,:price,:from,:to)')->execute(['p'=>$product['id'],'level'=>$d['price_level_id']?:null,'minimum'=>$min,'maximum'=>$max,'price'=>$d['unit_price'],'from'=>$d['valid_from']?:null,'to'=>$d['valid_to']?:null]);}elseif($kind==='price'){$pdo->prepare('INSERT INTO product_prices(product_id,price_level_id,fixed_price,discount_rate,minimum_quantity,valid_from,valid_to)VALUES(:p,:level,:fixed,:discount,:minimum,:from,:to) ON DUPLICATE KEY UPDATE fixed_price=VALUES(fixed_price),discount_rate=VALUES(discount_rate),minimum_quantity=VALUES(minimum_quantity),valid_from=VALUES(valid_from),valid_to=VALUES(valid_to)')->execute(['p'=>$product['id'],'level'=>$d['price_level_id'],'fixed'=>$d['fixed_price']?:null,'discount'=>$d['discount_rate']?:null,'minimum'=>$d['minimum_quantity']?:1,'from'=>$d['valid_from']?:null,'to'=>$d['valid_to']?:null]);}elseif($kind==='variant'){$key=trim((string)$d['combination_key']);if($key==='')throw new \InvalidArgumentException('Combinaison obligatoire.');$pdo->prepare('INSERT INTO product_variants(product_id,reference,barcode,combination_key,purchase_price,sale_price)VALUES(:p,:reference,:barcode,:key,:purchase,:sale)')->execute(['p'=>$product['id'],'reference'=>$d['reference'],'barcode'=>$d['barcode']?:null,'key'=>$key,'purchase'=>$d['purchase_price']?:null,'sale'=>$d['sale_price']?:null]);}else throw new \InvalidArgumentException('Type non pris en charge.');$this->service()->history($pdo,(int)$product['id'],(int)$actor['id'],$kind.'_added',null,$d);});}catch(\Throwable$e){$this->backWithErrors([$e instanceof \InvalidArgumentException?$e->getMessage():'Données déjà utilisées ou invalides.'],$d);}app()->audit()->log($kind.'_added','products','Élément catalogue ajouté.',(int)$actor['id'],(int)$product['company_id'],'product',(int)$product['id']);$this->redirect('products/'.$product['id'],'Élément ajouté.');}
 public function upload(Request$r):never{$p=$this->service()->find((int)$r->param('id'));$f=$r->file('file');$allowed=['image/jpeg'=>'jpg','image/png'=>'png','image/webp'=>'webp','application/pdf'=>'pdf'];if(!$f||$f['error']!==UPLOAD_ERR_OK||$f['size']>(int)app()->config('upload_max_bytes'))$this->backWithErrors(['Fichier invalide ou trop volumineux.']);$mime=(new \finfo(FILEINFO_MIME_TYPE))->file($f['tmp_name']);$ext=$allowed[$mime]??null;if(!$ext)$this->backWithErrors(['Format interdit.']);$kind=str_starts_with($mime,'image/')?'image':'document';$name=bin2hex(random_bytes(24)).'.'.$ext;$dir=dirname(__DIR__,2).'/storage/uploads/products';if(!is_dir($dir))mkdir($dir,0775,true);if(!move_uploaded_file($f['tmp_name'],$dir.'/'.$name))$this->backWithErrors(['Stockage impossible.']);app()->db()->connection()->prepare('INSERT INTO product_files(product_id,file_kind,document_type,original_name,stored_name,mime_type,size_bytes,sha256,uploaded_by)VALUES(:p,:kind,:type,:original,:stored,:mime,:size,:sha,:user)')->execute(['p'=>$p['id'],'kind'=>$kind,'type'=>$r->input('document_type')?:null,'original'=>basename($f['name']),'stored'=>$name,'mime'=>$mime,'size'=>$f['size'],'sha'=>hash_file('sha256',$dir.'/'.$name),'user'=>app()->auth()->user()['id']]);$this->redirect('products/'.$p['id'],'Fichier ajouté.');}
 public function download(Request$r):never{$p=$this->service()->find((int)$r->param('id'));$s=app()->db()->connection()->prepare('SELECT * FROM product_files WHERE id=:file AND product_id=:p');$s->execute(['file'=>$r->param('fileId'),'p'=>$p['id']]);$f=$s->fetch();$path=dirname(__DIR__,2).'/storage/uploads/products/'.($f['stored_name']??'');if(!$f||!is_file($path)){http_response_code(404);exit;}header('X-Content-Type-Options:nosniff');header('Content-Type:'.$f['mime_type']);header('Content-Disposition:attachment; filename="'.rawurlencode($f['original_name']).'"');readfile($path);exit;}
 public function export(Request$r):never{$format=(string)$r->param('format');$rows=$this->service()->paginate($r->all(),1,5000)['items'];$purchase=app()->auth()->can('products','view_purchase_price');$margin=app()->auth()->can('products','view_margin');$headers=['Référence','Nom','Type','Catégorie','Prix vente','TVA','Statut'];if($purchase)$headers[]='Prix achat';if($margin)$headers[]='Marge';$data=[];foreach($rows as$x){$row=[$x['reference'],$x['name'],$x['item_type'],$x['category_name'],$x['sale_price'],$x['tax_rate'],$x['status']];if($purchase)$row[]=$x['purchase_price'];if($margin)$row[]=$x['margin_value'];$data[]=$row;}if($format==='pdf')SimplePdf::output(array_merge(['Catalogue — '.date('Y-m-d'),implode(' | ',$headers)],array_map(fn($x)=>implode(' | ',$x),$data)),'catalogue.pdf');if($format==='xls'){header('Content-Type:application/vnd.ms-excel;charset=UTF-8');header('Content-Disposition:attachment;filename="catalogue.xls"');echo'<table><tr><th>'.implode('</th><th>',array_map('e',$headers)).'</th></tr>';foreach($data as$row)echo'<tr><td>'.implode('</td><td>',array_map('e',$row)).'</td></tr>';echo'</table>';exit;}header('Content-Type:text/csv;charset=UTF-8');header('Content-Disposition:attachment;filename="catalogue.csv"');$o=fopen('php://output','wb');fputcsv($o,$headers,';');foreach($data as$row)fputcsv($o,$row,';');exit;}
 public function importForm(Request$r):void{$preview=app()->session()->get('product_import',[]);$this->view('products/import',compact('preview'));}
 public function template(Request$r):never{header('Content-Type:text/csv;charset=UTF-8');header('Content-Disposition:attachment;filename="modele-produits.csv"');echo"reference;barcode;name;item_type;category;brand;unit;purchase_price;sale_price;tax_rate;status\n";exit;}
 public function previewImport(Request$r):never{$f=$r->file('csv');if(!$f||$f['error']!==UPLOAD_ERR_OK||$f['size']>2097152)$this->backWithErrors(['CSV invalide.']);$h=fopen($f['tmp_name'],'rb');$headers=fgetcsv($h,0,';');$expected=['reference','barcode','name','item_type','category','brand','unit','purchase_price','sale_price','tax_rate','status'];if($headers!==$expected){fclose($h);$this->backWithErrors(['En-têtes invalides. Utilisez le modèle.']);}$refs=$this->service()->references();$lookup=fn($rows,$field,$value)=>array_values(array_filter($rows,fn($x)=>mb_strtolower((string)$x[$field])===mb_strtolower(trim($value))))[0]??null;$rows=[];$line=1;while(($v=fgetcsv($h,0,';'))!==false&&count($rows)<1000){$line++;$d=array_combine($headers,array_pad($v,count($headers),''));$errors=[];if($d['name']===''||!in_array($d['item_type'],['stockable','non_stockable','service'],true))$errors[]='nom/type';foreach(['purchase_price','sale_price']as$k)if(!is_numeric($d[$k])||(float)$d[$k]<0)$errors[]=$k;$category=$d['category']===''?null:$lookup($refs['categories'],'name',$d['category']);$brand=$d['brand']===''?null:$lookup($refs['brands'],'name',$d['brand']);$unit=$lookup($refs['units'],'symbol',$d['unit']);$tax=$lookup($refs['taxes'],'rate',$d['tax_rate']);if($d['category']!==''&&!$category)$errors[]='catégorie inexistante';if($d['brand']!==''&&!$brand)$errors[]='marque inexistante';if(!$unit)$errors[]='unité inexistante';if(!$tax)$errors[]='TVA inexistante';$d['category_id']=$category['id']??'';$d['brand_id']=$brand['id']??'';$d['unit_id']=$unit['id']??'';$d['tax_rate_id']=$tax['id']??'';$d['currency_code']='MAD';$d['min_sale_price']=0;$d['max_discount']=0;$d['is_sellable']=1;$d['is_purchasable']=1;$rows[]=['line'=>$line,'data'=>$d,'errors'=>$errors];}fclose($h);app()->session()->put('product_import',$rows);$this->redirect('products/import','Prévisualisation prête.');}
 public function confirmImport(Request$r):never{$rows=app()->session()->get('product_import',[]);if(!$rows||array_filter($rows,fn($x)=>$x['errors']))$this->backWithErrors(['Import absent ou comportant des erreurs.']);try{app()->db()->transaction(function()use($rows){foreach($rows as$x)$this->service()->save($x['data']);});}catch(\Throwable$e){$this->backWithErrors(['Import annulé : '.$e->getMessage()]);}app()->session()->forget('product_import');$this->redirect('products',count($rows).' article(s) importé(s).');}
}
