<?php
declare(strict_types=1);

namespace App\Services;

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

final class InvoiceService{

    public function __construct(private readonly Database$db){
    }
    private function c():int{
        return(int)app()->auth()->user()['company_id'];
    }private function u():int{
        return(int)app()->auth()->user()['id'];
    }
    private function uuid():string{
        $x=bin2hex(random_bytes(16));
        return substr($x,0,8).'-'.substr($x,8,4).'-4'.substr($x,13,3).'-a'.substr($x,17,3).'-'.substr($x,20);
    }
    private function accountingNumber(PDO$p,string$type):string{
        $sequence=$type==='credit_note'?'credit_note':'standard';
        $s=$p->prepare('SELECT * FROM invoice_sequences WHERE company_id=:c AND invoice_type=:t FOR UPDATE');
        $s->execute(['c'=>$this->c(),'t'=>$sequence]);
        $x=$s->fetch();
        if(!$x)throw new InvalidArgumentException('Séquence de facture absente.');
        $y=(int)date('Y');
        if($x['reset_annually']&&(int)$x['current_year']!==$y){
            $p->prepare('UPDATE invoice_sequences SET next_value=1,current_year=:y WHERE id=:id')->execute(['y'=>$y,'id'=>$x['id']]);
            $x['next_value']=1;
        }$n=$x['prefix'].($x['include_year']?$y.'-':'').($x['include_month']?date('m').'-':'').str_pad((string)$x['next_value'],(int)$x['padding'],'0',STR_PAD_LEFT);
        $p->prepare('UPDATE invoice_sequences SET next_value=next_value+1 WHERE id=:id')->execute(['id'=>$x['id']]);
        return$n;
    }
    public function save(array$d,?int$id=null):int{
        return$this->db->transaction(function(PDO$p)use($d,$id){
            $old=null;
            if($id){
                $s=$p->prepare('SELECT * FROM customer_invoices WHERE id=:id AND company_id=:c FOR UPDATE');
                $s->execute(['id'=>$id,'c'=>$this->c()]);
                $old=$s->fetch();
                if(!$old||$old['status']!=='draft')throw new InvalidArgumentException('Seule une facture brouillon peut être modifiée.');
            }$customer=(int)($d['customer_id']??0);
            $cs=$p->prepare('SELECT * FROM third_parties WHERE id=:id AND company_id=:c AND party_type="customer"');
            $cs->execute(['id'=>$customer,'c'=>$this->c()]);
            $cust=$cs->fetch();
            if(!$cust)throw new InvalidArgumentException('Client invalide.');
            $type=$d['invoice_type']??'standard';
            if(!in_array($type,['standard','deposit','balance','recurring','manual','proforma','credit_note'],true))throw new InvalidArgumentException('Type invalide.');
            $lines=[];
            $gross=$discountTotal=$taxTotal=0;
            foreach((array)($d['line_type']??[])as$i=>$lineType){
                $name=trim((string)($d['designation'][$i]??''));
                if($name==='')continue;
                $qty=(float)($d['quantity'][$i]??0);
                $price=(float)($d['unit_price'][$i]??0);
                $discount=(float)($d['discount_rate'][$i]??0);
                $tax=(float)($d['tax_rate'][$i]??0);
                if(in_array($lineType,['product','service','free'],true)&&($qty<=0||$price<0||$discount<0||$discount>100||$tax<0))throw new InvalidArgumentException('Valeurs de ligne invalides.');
                $base=$qty*$price;
                $disc=$base*$discount/100;
                $ht=$base-$disc;
                $vat=$ht*$tax/100;
                $gross+=$base;
                $discountTotal+=$disc;
                $taxTotal+=$vat;
                $lines[]=['type'=>$lineType,'product'=>(int)($d['product_id'][$i]??0)?:null,'variant'=>(int)($d['variant_id'][$i]??0)?:null,'pack'=>(int)($d['packaging_id'][$i]??0)?:null,'order_line'=>(int)($d['customer_order_line_id'][$i]??0)?:null,'delivery_line'=>(int)($d['delivery_note_line_id'][$i]??0)?:null,'reference'=>$d['reference'][$i]??null,'name'=>$name,'description'=>$d['description'][$i]??null,'unit'=>$d['unit'][$i]??null,'qty'=>$qty,'price'=>$price,'discount'=>$discount,'disc'=>$disc,'tax'=>$tax,'ht'=>$ht,'vat'=>$vat];
            }$shipping=(float)($d['shipping_cost']??0);
            $additional=(float)($d['additional_cost']??0);
            $stamp=(float)($d['stamp_amount']??0);
            $lineSubtotal=$gross-$discountTotal;
            $globalType=(string)($d['global_discount_type']??'percent');
            $globalValue=(float)($d['global_discount_value']??0);
            if(!in_array($globalType,['percent','amount'],true)||$globalValue<0||($globalType==='percent'&&$globalValue>100)){
                throw new InvalidArgumentException('Remise globale invalide.');
            }
            $globalDiscount=$globalType==='percent'?$lineSubtotal*$globalValue/100:$globalValue;
            if($globalDiscount>$lineSubtotal){
                throw new InvalidArgumentException('La remise globale dépasse le sous-total.');
            }
            if($lineSubtotal>0){
                $taxTotal*=1-($globalDiscount/$lineSubtotal);
            }
            $subtotal=$lineSubtotal-$globalDiscount;
            $total=$subtotal+$taxTotal+$shipping+$additional+$stamp;
            $deposit=(float)($d['deposit_deduction']??0);
            $credit=(float)($d['credit_total']??0);
            $net=$total-$deposit-$credit;
            if(!$lines||$net<0)throw new InvalidArgumentException('Totaux ou lignes invalides.');
            $params=['c'=>$this->c(),'draft'=>$old['draft_reference']??('BROUILLON-'.date('Ymd').'-'.strtoupper(bin2hex(random_bytes(3)))),'key'=>$old['idempotency_key']??$this->uuid(),'type'=>$type,'customer'=>$customer,'contact'=>(int)($d['contact_id']??0)?:null,'billing'=>(int)($d['billing_address_id']??0)?:null,'quote'=>(int)($d['quote_id']??0)?:null,'order'=>(int)($d['customer_order_id']??0)?:null,'original'=>(int)($d['original_invoice_id']??0)?:null,'date'=>$d['invoice_date']??date('Y-m-d'),'due'=>!empty($d['due_date'])?$d['due_date']:null,'terms'=>$d['payment_terms']??null,'method'=>$d['planned_payment_method']??null,'currency'=>$d['currency_code']??'MAD','reference'=>$d['customer_reference']??null,'sales'=>(int)($d['salesperson_id']??$this->u()),'subject'=>$d['subject']??null,'internal'=>$d['internal_notes']??null,'visible'=>$d['visible_notes']??null,'legal'=>$d['legal_mentions']??null,'gross'=>round($gross,4),'discount'=>round($discountTotal,4),'global_discount'=>round($globalDiscount,4),'subtotal'=>round($subtotal,4),'tax'=>round($taxTotal,4),'shipping'=>$shipping,'additional'=>$additional,'stamp'=>$stamp,'total'=>round($total,4),'deposit'=>$deposit,'credit'=>$credit,'net'=>round($net,4),'balance'=>round($net,4),'snapshot'=>json_encode($cust,JSON_UNESCAPED_UNICODE),'user'=>$this->u()];
            if($id){
                $params['id']=$id;
                $p->prepare('UPDATE customer_invoices SET invoice_type=:type,customer_id=:customer,contact_id=:contact,billing_address_id=:billing,quote_id=:quote,customer_order_id=:order,original_invoice_id=:original,invoice_date=:date,due_date=:due,payment_terms=:terms,planned_payment_method=:method,currency_code=:currency,customer_reference=:reference,salesperson_id=:sales,subject=:subject,internal_notes=:internal,visible_notes=:visible,legal_mentions=:legal,gross_subtotal=:gross,line_discount_total=:discount,global_discount_total=:global_discount,subtotal=:subtotal,tax_total=:tax,shipping_cost=:shipping,additional_cost=:additional,stamp_amount=:stamp,total=:total,deposit_deduction=:deposit,credit_total=:credit,net_due=:net,balance_due=:balance,customer_snapshot=:snapshot WHERE id=:id AND company_id=:c')->execute($params);
                $p->prepare('DELETE FROM customer_invoice_lines WHERE customer_invoice_id=:id')->execute(['id'=>$id]);
                $invoice=$id;
            }else{
                $p->prepare('INSERT INTO customer_invoices(company_id,draft_reference,idempotency_key,invoice_type,customer_id,contact_id,billing_address_id,quote_id,customer_order_id,original_invoice_id,invoice_date,due_date,payment_terms,planned_payment_method,currency_code,customer_reference,salesperson_id,subject,internal_notes,visible_notes,legal_mentions,gross_subtotal,line_discount_total,global_discount_total,subtotal,tax_total,shipping_cost,additional_cost,stamp_amount,total,deposit_deduction,credit_total,net_due,balance_due,customer_snapshot,created_by)VALUES(:c,:draft,:key,:type,:customer,:contact,:billing,:quote,:order,:original,:date,:due,:terms,:method,:currency,:reference,:sales,:subject,:internal,:visible,:legal,:gross,:discount,:global_discount,:subtotal,:tax,:shipping,:additional,:stamp,:total,:deposit,:credit,:net,:balance,:snapshot,:user)')->execute($params);
                $invoice=(int)$p->lastInsertId();
            }$ins=$p->prepare('INSERT INTO customer_invoice_lines(customer_invoice_id,line_type,sort_order,product_id,variant_id,packaging_id,customer_order_line_id,delivery_note_line_id,reference_snapshot,designation_snapshot,description_snapshot,unit_snapshot,quantity,unit_price,discount_rate,discount_total,tax_rate,line_subtotal,line_tax,line_total)VALUES(:invoice,:type,:sort,:product,:variant,:pack,:order_line,:delivery_line,:reference,:name,:description,:unit,:qty,:price,:discount,:disc,:tax,:ht,:vat,:total)');
            foreach($lines as$i=>$l)$ins->execute(['invoice'=>$invoice,'type'=>$l['type'],'sort'=>$i+1,'product'=>$l['product'],'variant'=>$l['variant'],'pack'=>$l['pack'],'order_line'=>$l['order_line'],'delivery_line'=>$l['delivery_line'],'reference'=>$l['reference'],'name'=>$l['name'],'description'=>$l['description'],'unit'=>$l['unit'],'qty'=>$l['qty'],'price'=>$l['price'],'discount'=>$l['discount'],'disc'=>$l['disc'],'tax'=>$l['tax'],'ht'=>$l['ht'],'vat'=>$l['vat'],'total'=>$l['ht']+$l['vat']]);
            $this->history($p,$invoice,$id?'update':'create',null,'draft');
            return$invoice;
        });
    }
    public function fromOrder(int$orderId):int{
        $p=$this->db->connection();
        $s=$p->prepare('SELECT * FROM customer_orders WHERE id=:id AND company_id=:c AND status NOT IN("draft","cancelled","archived")');
        $s->execute(['id'=>$orderId,'c'=>$this->c()]);
        $o=$s->fetch();
        if(!$o)throw new InvalidArgumentException('Commande non facturable.');
        $l=$p->prepare('SELECT l.*,COALESCE((SELECT SUM(il.quantity) FROM customer_invoice_lines il JOIN customer_invoices i ON i.id=il.customer_invoice_id WHERE il.customer_order_line_id=l.id AND i.status NOT IN("cancelled","archived")),0) invoiced FROM customer_order_lines l WHERE l.customer_order_id=:id ORDER BY l.sort_order');
        $l->execute(['id'=>$orderId]);
        return$this->save($this->sourceData($o,$l->fetchAll(),'order'));
    }
    public function fromDelivery(int$deliveryId):int{
        $p=$this->db->connection();
        $s=$p->prepare('SELECT d.*,o.salesperson_id,o.payment_terms,o.currency_code FROM delivery_notes d LEFT JOIN customer_orders o ON o.id=d.customer_order_id WHERE d.id=:id AND d.company_id=:c AND d.status IN("validated","in_delivery","delivered")');
        $s->execute(['id'=>$deliveryId,'c'=>$this->c()]);
        $d=$s->fetch();
        if(!$d)throw new InvalidArgumentException('Bon de livraison non facturable.');
        $l=$p->prepare('SELECT l.*,COALESCE((SELECT SUM(il.quantity) FROM customer_invoice_lines il JOIN customer_invoices i ON i.id=il.customer_invoice_id WHERE il.delivery_note_line_id=l.id AND i.status NOT IN("cancelled","archived")),0) invoiced FROM delivery_note_lines l WHERE l.delivery_note_id=:id ORDER BY l.sort_order');
        $l->execute(['id'=>$deliveryId]);
        return$this->save($this->sourceData($d,$l->fetchAll(),'delivery'));
    }
    private function sourceData(array$h,array$lines,string$source):array{
        $d=['invoice_type'=>'standard','customer_id'=>$h['customer_id'],'contact_id'=>$h['contact_id']??null,'billing_address_id'=>$h['billing_address_id']??null,'customer_order_id'=>$source==='order'?$h['id']:($h['customer_order_id']??null),'invoice_date'=>date('Y-m-d'),'due_date'=>date('Y-m-d',strtotime('+30 days')),'payment_terms'=>$h['payment_terms']??null,'currency_code'=>$h['currency_code']??'MAD','customer_reference'=>$h['customer_reference']??null,'salesperson_id'=>$h['salesperson_id']??$this->u()];
        foreach($lines as$x){
            $ordered=(float)($source==='order'?$x['quantity']:$x['delivery_quantity']);
            $qty=$ordered-(float)$x['invoiced'];
            if($qty<=0)continue;
            $d['line_type'][]=$source==='order'?$x['line_type']:'product';
            $d['product_id'][]=$x['product_id'];
            $d['variant_id'][]=$x['variant_id'];
            $d['packaging_id'][]=$x['packaging_id'];
            $d['customer_order_line_id'][]=$source==='order'?$x['id']:$x['customer_order_line_id'];
            $d['delivery_note_line_id'][]=$source==='delivery'?$x['id']:null;
            $d['reference'][]=$x['reference_snapshot'];
            $d['designation'][]=$x['designation_snapshot'];
            $d['description'][]=$x['description_snapshot'];
            $d['unit'][]=$x['unit_snapshot'];
            $d['quantity'][]=$qty;
            $d['unit_price'][]=$source==='order'?$x['unit_price']:$x['unit_price_snapshot'];
            $d['discount_rate'][]=$source==='order'?$x['discount_rate']:0;
            $d['tax_rate'][]=$source==='order'?$x['tax_rate']:20;
        }if(empty($d['line_type']))throw new InvalidArgumentException('Toutes les quantités sont déjà facturées.');
        return$d;
    }
    public function validate(int$id):void{
        $this->db->transaction(function(PDO$p)use($id){
            $s=$p->prepare('SELECT * FROM customer_invoices WHERE id=:id AND company_id=:c FOR UPDATE');
            $s->execute(['id'=>$id,'c'=>$this->c()]);
            $i=$s->fetch();
            if(!$i)throw new InvalidArgumentException('Facture absente.');
            if($i['status']==='validated')return;
            if($i['status']!=='draft')throw new InvalidArgumentException('Validation interdite.');
            if((float)$i['net_due']<0)throw new InvalidArgumentException('Net à payer invalide.');
            $company=$p->prepare('SELECT * FROM companies WHERE id=:c');
            $company->execute(['c'=>$this->c()]);
            $number=$i['invoice_type']==='proforma'?('PRO-'.date('Y').'-'.str_pad((string)$id,6,'0',STR_PAD_LEFT)):$this->accountingNumber($p,$i['invoice_type']);
            $p->prepare('UPDATE customer_invoices SET accounting_number=:n,status="validated",validated_by=:u,validated_at=NOW(),company_snapshot=:snapshot WHERE id=:id')->execute(['n'=>$number,'u'=>$this->u(),'snapshot'=>json_encode($company->fetch(),JSON_UNESCAPED_UNICODE),'id'=>$id]);
            $p->prepare('INSERT INTO invoice_due_dates(customer_invoice_id,due_date,amount,percentage)VALUES(:id,:due,:amount,100)')->execute(['id'=>$id,'due'=>$i['due_date']?:$i['invoice_date'],'amount'=>$i['net_due']]);
            $this->history($p,$id,'validate','draft','validated');
        });
    }
    public function full(int$id):array{
        $p=$this->db->connection();
        $s=$p->prepare('SELECT i.*,COALESCE(c.legal_name,CONCAT(c.first_name," ",c.last_name))customer,CONCAT(u.first_name," ",u.last_name)salesperson FROM customer_invoices i JOIN third_parties c ON c.id=i.customer_id JOIN users u ON u.id=i.salesperson_id WHERE i.id=:id AND i.company_id=:c');
        $s->execute(['id'=>$id,'c'=>$this->c()]);
        $i=$s->fetch();
        if(!$i)throw new InvalidArgumentException('Facture introuvable.');
        $l=$p->prepare('SELECT * FROM customer_invoice_lines WHERE customer_invoice_id=:id ORDER BY sort_order');
        $l->execute(['id'=>$id]);
        $i['lines']=$l->fetchAll();
        return$i;
    }
    private function history(PDO$p,int$id,string$a,?string$old,?string$new):void{
        $p->prepare('INSERT INTO invoice_status_history(customer_invoice_id,user_id,action,old_status,new_status)VALUES(:i,:u,:a,:o,:n)')->execute(['i'=>$id,'u'=>$this->u(),'a'=>$a,'o'=>$old,'n'=>$new]);
    }
}
