<?php

declare(strict_types=1);
namespace App\Services;
use App\Repositories\InvoiceRepository;
use App\Repositories\SessionRepository;
use App\Repositories\AssessmentRepository;
use App\Repositories\BillingRateRepository;
use InvalidArgumentException;
use RuntimeException;

final class InvoiceService
{
    public function __construct(private ?InvoiceRepository $repo=null, private ?SessionRepository $sessions=null, private ?AssessmentRepository $assessments=null, private ?BillingRateRepository $billingRates=null){$this->repo??=new InvoiceRepository();$this->sessions??=new SessionRepository();$this->assessments??=new AssessmentRepository();$this->billingRates??=new BillingRateRepository();}
    public function list(array $filters):array{return $this->repo->paginate($filters,25);}
    public function find(int $id):array{$r=$this->repo->find($id);if($r===null)throw new RuntimeException('Invoice not found.',404);$r['items']=$this->repo->items($id);return $r;}
    public function formData():array{return ['patients'=>$this->repo->patients(),'services'=>$this->repo->services()];}
    public function create(array $input,int $actor):int{$v=$this->validate($input);$v['data']['created_by']=$actor;$id=$this->repo->create($v['data'],$v['items']);$this->repo->audit($actor,'invoice_created',$id,['invoice_number'=>$this->repo->find($id)['invoice_number']]);return $id;}
    public function update(int $id,array $input,int $actor):void{$this->find($id);$v=$this->validate($input);$this->repo->update($id,$v['data'],$v['items']);$this->repo->audit($actor,'invoice_updated',$id);}
    public function billableSessions(int $patientId): array{return $this->sessions->billableSessions($patientId);}
    public function billableAssessments(int $patientId): array{return $this->assessments->billableAssessments($patientId);}

    public function createFromSessions(array $input, int $actor): int
    {
        $patient=(int)($input['patient_id']??0);
        if($patient<1) throw new InvalidArgumentException('Select a patient.');
        $ids=array_values(array_filter(array_map('intval',(array)$input['session_ids']??[])));
        if(!$ids) throw new InvalidArgumentException('Select at least one completed billable session.');
        $sessions=$this->sessions->billableSessions($patient);
        $allowed=[]; foreach($sessions as $row) $allowed[(int)$row['id']]=$row;
        $items=[]; $subtotal=0.0;
        foreach($ids as $id){
            if(!isset($allowed[$id])) throw new InvalidArgumentException('One or more selected sessions are no longer available for invoicing.');
            $row=$allowed[$id]; $this->billingRates->ensureDefaults($patient); $price=$this->billingRates->rateFor($patient,(int)$row['service_id']) ?? (float)$row['service_price']; $price=round($price,2);
            $items[]=['service_id'=>(int)$row['service_id'],'session_id'=>(int)$row['id'],'description'=>$row['service_name'],'service_date'=>$row['session_date'],'quantity'=>'1.00','unit_price'=>number_format($price,2,'.',''),'line_total'=>number_format($price,2,'.','')];
            $subtotal += $price;
        }
        $discount=$this->money($input['discount']??'0','Discount'); $tax=$this->money($input['tax']??'0','Tax');
        $date=trim((string)($input['invoice_date']??date('Y-m-d'))); if(!preg_match('/^\d{4}-\d{2}-\d{2}$/',$date)) throw new InvalidArgumentException('Enter a valid invoice date.');
        $due=trim((string)($input['due_date']??'')); if($due!==''&&!preg_match('/^\d{4}-\d{2}-\d{2}$/',$due)) throw new InvalidArgumentException('Enter a valid due date.');
        $data=['patient_id'=>$patient,'invoice_date'=>$date,'due_date'=>$due!==''?$due:null,'subtotal'=>number_format($subtotal,2,'.',''),'discount'=>number_format($discount,2,'.',''),'tax'=>number_format($tax,2,'.',''),'total'=>number_format(max(0,round($subtotal-$discount+$tax,2)),2,'.',''),'notes'=>trim((string)($input['notes']??''))?:null,'created_by'=>$actor];
        $id=$this->repo->createFromSessions($data,$items);
        $this->repo->audit($actor,'invoice_created_from_sessions',$id,['session_ids'=>$ids,'patient_id'=>$patient]);
        return $id;
    }

    public function createFromClinicalWork(array $input,int $actor): int
    {
        $patient=(int)($input['patient_id']??0); if($patient<1) throw new InvalidArgumentException('Select a patient.');
        $sessionIds=array_values(array_filter(array_map('intval',(array)$input['session_ids']??[]))); $assessmentIds=array_values(array_filter(array_map('intval',(array)$input['assessment_ids']??[])));
        if(!$sessionIds && !$assessmentIds) throw new InvalidArgumentException('Select at least one completed service or approved assessment.');
        $sessions=$this->sessions->billableSessions($patient); $sessionMap=[]; foreach($sessions as $r)$sessionMap[(int)$r['id']]=$r;
        $assessments=$this->assessments->billableAssessments($patient); $assessmentMap=[]; foreach($assessments as $r)$assessmentMap[(int)$r['id']]=$r;
        $items=[];$subtotal=0.0;
        foreach($sessionIds as $id){if(!isset($sessionMap[$id]))throw new InvalidArgumentException('A selected session is no longer available for billing.');$r=$sessionMap[$id];$this->billingRates->ensureDefaults($patient);$price=$this->billingRates->rateFor($patient,(int)$r['service_id']) ?? (float)$r['service_price'];$price=round($price,2);$items[]=['service_id'=>(int)$r['service_id'],'session_id'=>$id,'description'=>$r['service_name'],'service_date'=>$r['session_date'],'quantity'=>'1.00','unit_price'=>number_format($price,2,'.',''),'line_total'=>number_format($price,2,'.','')];$subtotal+=$price;}
        foreach($assessmentIds as $id){if(!isset($assessmentMap[$id]))throw new InvalidArgumentException('A selected assessment is no longer available for billing.');$r=$assessmentMap[$id];$this->billingRates->ensureDefaults($patient);$price=$this->billingRates->rateFor($patient,(int)$r['service_id']) ?? (float)$r['service_price'];$price=round($price,2);$items[]=['service_id'=>(int)$r['service_id'],'assessment_id'=>$id,'description'=>$r['service_name'].' — '.$r['assessment_reference'],'service_date'=>$r['assessment_date'],'quantity'=>'1.00','unit_price'=>number_format($price,2,'.',''),'line_total'=>number_format($price,2,'.','')];$subtotal+=$price;}
        $discount=$this->money($input['discount']??'0','Discount');$tax=$this->money($input['tax']??'0','Tax');$date=trim((string)($input['invoice_date']??date('Y-m-d')));if(!preg_match('/^\d{4}-\d{2}-\d{2}$/',$date))throw new InvalidArgumentException('Enter a valid invoice date.');$due=trim((string)($input['due_date']??''));if($due!==''&&!preg_match('/^\d{4}-\d{2}-\d{2}$/',$due))throw new InvalidArgumentException('Enter a valid due date.');
        $data=['patient_id'=>$patient,'invoice_date'=>$date,'due_date'=>$due!==''?$due:null,'subtotal'=>number_format($subtotal,2,'.',''),'discount'=>number_format($discount,2,'.',''),'tax'=>number_format($tax,2,'.',''),'total'=>number_format(max(0,round($subtotal-$discount+$tax,2)),2,'.',''),'notes'=>trim((string)($input['notes']??''))?:null,'created_by'=>$actor];
        $id=$this->repo->createFromSessions($data,$items);$this->repo->audit($actor,'invoice_created_from_clinical_work',$id,['session_ids'=>$sessionIds,'assessment_ids'=>$assessmentIds,'patient_id'=>$patient]);return $id;
    }

    public function setStatus(int $id,string $status,int $actor):void{$this->find($id);if(!in_array($status,['unpaid','cancelled'],true))throw new InvalidArgumentException('Only unpaid or cancelled status can be set during the invoice phase. Payment statuses are managed in Phase 16.');$this->repo->setStatus($id,$status);$this->repo->audit($actor,'invoice_status_changed',$id,['status'=>$status]);}
    private function validate(array $d):array
    {
        $patient=(int)($d['patient_id']??0);if($patient<1)throw new InvalidArgumentException('Select a patient.');
        $date=trim((string)($d['invoice_date']??''));if(!preg_match('/^\d{4}-\d{2}-\d{2}$/',$date))throw new InvalidArgumentException('Enter a valid invoice date.');
        $due=trim((string)($d['due_date']??''));if($due!==''&&!preg_match('/^\d{4}-\d{2}-\d{2}$/',$due))throw new InvalidArgumentException('Enter a valid due date.');
        $discount=$this->money($d['discount']??'0','Discount');$tax=$this->money($d['tax']??'0','Tax');
        // Invoice rows are submitted as items[index][field]. Normalize them into
        // the same parallel arrays used by the validation below. Keep support for
        // the older flat [] format so existing forms remain compatible.
        $submittedItems=is_array($d['items']??null)?$d['items']:[];
        if($submittedItems){
            $ids=[];$descs=[];$dates=[];$qtys=[];$prices=[];
            foreach($submittedItems as $row){
                if(!is_array($row))continue;
                $ids[]=$row['service_id']??'';
                $descs[]=$row['description']??'';
                $dates[]=$row['service_date']??'';
                $qtys[]=$row['quantity']??'1';
                $prices[]=$row['unit_price']??'';
            }
        }else{
            $ids=is_array($d['service_id']??null)?$d['service_id']:[];
            $descs=is_array($d['description']??null)?$d['description']:[];
            $dates=is_array($d['service_date']??null)?$d['service_date']:[];
            $qtys=is_array($d['quantity']??null)?$d['quantity']:[];
            $prices=is_array($d['unit_price']??null)?$d['unit_price']:[];
        }
        $rowCount=max(count($ids),count($descs),count($dates),count($qtys),count($prices));
        if($rowCount===0)throw new InvalidArgumentException('Add at least one invoice item.');
        $catalog=[];foreach($this->repo->services() as $s)$catalog[(int)$s['id']]=$s;
        $items=[];$subtotal=0.0;
        for($i=0;$i<$rowCount;$i++){
            $desc=trim((string)($descs[$i]??''));
            $sid=(int)($ids[$i]??0);
            if($sid>0&&isset($catalog[$sid])){
                $desc=$catalog[$sid]['name'];
                if(trim((string)($prices[$i]??''))==='')$prices[$i]=$catalog[$sid]['price'];
            }
            if($desc==='')throw new InvalidArgumentException('Invoice item '.($i+1).' is missing a description. Select a service or enter a description.');
            $qty=(float)($qtys[$i]??1);
            if($qty<=0)throw new InvalidArgumentException('Invoice item '.($i+1).' quantity must be greater than zero.');
            $price=$this->money($prices[$i]??'0','Invoice item '.($i+1).' unit price');
            $line=round($qty*$price,2);$subtotal+=$line;
            $sd=trim((string)($dates[$i]??''));
            if($sd!==''&&!preg_match('/^\d{4}-\d{2}-\d{2}$/',$sd))throw new InvalidArgumentException('Invoice item '.($i+1).' has an invalid service date.');
            $items[]=['service_id'=>$sid>0&&isset($catalog[$sid])?$sid:null,'description'=>$desc,'service_date'=>$sd!==''?$sd:null,'quantity'=>number_format($qty,2,'.',''),'unit_price'=>number_format($price,2,'.',''),'line_total'=>number_format($line,2,'.','')];
        }
        if(!$items)throw new InvalidArgumentException('Add at least one valid invoice item.');$total=max(0,round($subtotal-$discount+$tax,2));
        return ['data'=>['patient_id'=>$patient,'invoice_date'=>$date,'due_date'=>$due!==''?$due:null,'subtotal'=>number_format($subtotal,2,'.',''),'discount'=>number_format($discount,2,'.',''),'tax'=>number_format($tax,2,'.',''),'total'=>number_format($total,2,'.',''),'notes'=>trim((string)($d['notes']??''))?:null,'created_by'=>0],'items'=>$items];
    }
    private function money(mixed $v,string $label):float{$s=trim((string)$v);if($s==='' )return 0.0;if(!preg_match('/^\d+(?:\.\d{1,2})?$/',$s))throw new InvalidArgumentException($label.' must be a valid amount with up to 2 decimal places.');return (float)$s;}
}
