<?php

namespace App\Services;

use App\Models\ContractTemplate;
use App\Models\Loan;
use DOMDocument;
use DOMElement;
use DOMXPath;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\ValidationException;
use ZipArchive;

class ContractPreviewEditorService
{
    public function __construct(
        private DocxContractTemplateService $templates,
        private PdfContractTemplateService $pdfTemplates,
    ) {}

    public function fieldCatalog(): array
    {
        return $this->templates->fieldCatalog();
    }

    public function editorRows(ContractTemplate $template, Loan $loan, array $overrides = [], array $formatting = []): array
    {
        $values=$this->fieldValues($loan);
        $catalog=$this->fieldCatalog();

        return collect($template->mappings ?? [])->values()->map(function(array $mapping,int $index) use($template,$values,$catalog,$overrides,$formatting): array {
            $field=(string)($mapping['field'] ?? '');
            $systemValue=(string)($values[$field] ?? '');
            $common=[
                'index'=>$index,'field'=>$field,'field_label'=>$catalog[$field] ?? $field,'label'=>(string)($mapping['label'] ?? 'Mapped field'),
                'source_text'=>(string)($mapping['source_text'] ?? ''),'block_id'=>(string)($mapping['block_id'] ?? ''),'system_value'=>$systemValue,
                'value'=>array_key_exists($index,$overrides) ? (string)$overrides[$index] : $systemValue,'mode'=>(string)($mapping['mode'] ?? 'replace_blank'),
            ];
            if ($template->isPdf()) {
                $format=$this->pdfTemplates->formatFor($mapping,$formatting[$index] ?? []);
                return $common+[
                    'page'=>$format['page'],'x'=>$format['x'],'y'=>$format['y'],'width'=>$format['width'],'font_size'=>$format['font_size'],
                    'line_height'=>$format['line_height'],'align'=>$format['align'],
                ];
            }
            $format=$this->docxFormatFor($mapping,$formatting[$index] ?? []);
            return $common+[
                'spaces_before'=>$format['spaces_before'],'spaces_after'=>$format['spaces_after'],'trim_surrounding'=>$format['trim_surrounding'],
            ];
        })->all();
    }

    public function preview(ContractTemplate $template, Loan $loan, array $overrides = [], array $formatting = []): array
    {
        if ($template->isPdf()) return [];
        $source=Storage::disk('local')->path($template->file_path);
        abort_unless(is_file($source),404,'The source contract template file is missing.');
        [$document,$xpath]=$this->dom($this->documentXml($source));
        $this->applyDocxMappings($document,$xpath,$template,$loan,$overrides,$formatting);
        $body=$xpath->query('/w:document/w:body')->item(0);
        if (! $body instanceof DOMElement) return [];

        $blocks=[];
        foreach ($body->childNodes as $child) {
            if (! $child instanceof DOMElement) continue;
            if ($child->localName==='p') {
                $blocks[]=$this->paragraphPreview($child,$xpath);
                $breaks=$xpath->query('.//w:br[@w:type="page"]',$child);
                if ($breaks && $breaks->length) $blocks[]=['type'=>'page_break'];
            } elseif ($child->localName==='tbl') {
                $blocks[]=$this->tablePreview($child,$xpath);
            } elseif ($child->localName==='sdt') {
                $nested=$xpath->query('.//w:sdtContent/*',$child);
                if ($nested) foreach ($nested as $node) {
                    if (! $node instanceof DOMElement) continue;
                    if ($node->localName==='p') $blocks[]=$this->paragraphPreview($node,$xpath);
                    if ($node->localName==='tbl') $blocks[]=$this->tablePreview($node,$xpath);
                }
            }
        }
        return $blocks;
    }

    public function generate(ContractTemplate $template, Loan $loan, array $overrides = [], array $formatting = []): string
    {
        if ($template->isPdf()) {
            return $this->pdfTemplates->generate($template,$this->fieldValues($loan),$overrides,$formatting);
        }

        $this->assertZipAvailable();
        $source=Storage::disk('local')->path($template->file_path);
        abort_unless(is_file($source),404,'The source contract template file is missing.');
        $temporary=tempnam(sys_get_temp_dir(),'mf-contract-editor-');
        if ($temporary===false) throw ValidationException::withMessages(['contract'=>'Unable to allocate a temporary contract file.']);
        $docx=$temporary.'.docx';
        @unlink($temporary);
        if (! copy($source,$docx)) throw ValidationException::withMessages(['contract'=>'Unable to prepare the contract template.']);

        $zip=new ZipArchive();
        if ($zip->open($docx)!==true) {
            @unlink($docx);
            throw ValidationException::withMessages(['contract'=>'The saved contract template is not a valid DOCX package.']);
        }
        try {
            $xml=$zip->getFromName('word/document.xml');
            if (! is_string($xml)) throw ValidationException::withMessages(['contract'=>'The DOCX template does not contain word/document.xml.']);
            [$document,$xpath]=$this->dom($xml);
            $this->applyDocxMappings($document,$xpath,$template,$loan,$overrides,$formatting);
            $zip->addFromString('word/document.xml',$document->saveXML());
        } finally {
            $zip->close();
        }
        $content=file_get_contents($docx);
        @unlink($docx);
        if (! is_string($content) || $content==='') throw ValidationException::withMessages(['contract'=>'The generated DOCX could not be read.']);
        return $content;
    }

    public function fieldValues(Loan $loan): array
    {
        $loan->loadMissing(['customer.address','branch.business','branch.manager','product','guarantor','collaterals.category','installments','creator','approver']);
        $business=$loan->branch->business;
        $customer=$loan->customer;
        $address=$customer->address;
        $guarantor=$loan->guarantor;
        $first=$loan->installments->first();
        $last=$loan->installments->last();
        $money=fn($value): string=>number_format((float)($value ?? 0),2,'.',',');
        $values=[
            'business.name'=>(string)$business->name,'business.phone'=>(string)($business->phone ?? ''),'business.email'=>(string)($business->email ?? ''),
            'business.address'=>collect([$business->address,$business->district,$business->region,$business->country])->filter()->join(', '),
            'business.registration_number'=>(string)($business->registration_number ?? ''),'business.tin'=>(string)($business->tin ?? ''),
            'branch.name'=>(string)$loan->branch->name,'branch.phone'=>(string)($loan->branch->phone ?? ''),
            'branch.address'=>collect([$loan->branch->address,$loan->branch->district,$loan->branch->region])->filter()->join(', '),
            'loan.number'=>(string)$loan->loan_number,'loan.principal'=>$money($loan->principal),'loan.interest'=>$money($loan->interest),
            'loan.processing_fee'=>$money($loan->processing_fee),'loan.total_repayment'=>$money($loan->total_repayment),
            'loan.installment_amount'=>$first ? $money($first->amount_due) : '',
            'loan.repayment_frequency'=>str((string)$loan->product->repayment_frequency)->replace('_',' ')->title()->toString(),
            'loan.term'=>trim((string)$loan->product->duration.' '.str((string)$loan->product->duration_unit)->singular()),
            'loan.schedule_start_date'=>$loan->schedule_start_date?->format('d/m/Y') ?? '','loan.final_due_date'=>$last?->due_date?->format('d/m/Y') ?? '',
            'loan.purpose'=>(string)$loan->purpose,'customer.full_name'=>(string)$customer->full_name,'customer.phone'=>(string)($customer->phone ?? ''),
            'customer.nin'=>(string)($customer->nin ?? ''),'customer.gender'=>(string)($customer->gender ?? ''),
            'customer.date_of_birth'=>$customer->date_of_birth?->format('d/m/Y') ?? '','customer.age'=>$customer->date_of_birth ? (string)$customer->date_of_birth->age : '',
            'customer.marital_status'=>(string)($customer->marital_status ?? ''),'customer.occupation'=>(string)($customer->occupation ?? ''),
            'customer.employer_business'=>(string)($customer->employer_business ?? ''),'customer.monthly_income'=>$customer->monthly_income!==null ? $money($customer->monthly_income) : '',
            'customer.address'=>$address ? collect([$address->house_number,$address->street_village,$address->ward,$address->district,$address->region])->filter()->join(', ') : '',
            'customer.region'=>(string)($address?->region ?? ''),'customer.district'=>(string)($address?->district ?? ''),'customer.ward'=>(string)($address?->ward ?? ''),
            'customer.street_village'=>(string)($address?->street_village ?? ''),'guarantor.full_name'=>(string)($guarantor?->full_name ?? ''),
            'guarantor.phone'=>(string)($guarantor?->phone ?? ''),'guarantor.nin'=>(string)($guarantor?->nin ?? ''),'guarantor.address'=>(string)($guarantor?->address ?? ''),
            'guarantor.occupation'=>(string)($guarantor?->occupation ?? ''),'guarantor.relationship'=>(string)($guarantor?->relationship ?? ''),
            'loan_officer.name'=>(string)($loan->creator?->name ?? ''),'approver.name'=>(string)($loan->approver?->name ?? ''),
            'collateral.summary'=>$loan->collaterals->map(fn($item)=>trim($item->code.' - '.$item->item.' ('.$money($item->accepted_value).')'))->join('; '),
        ];
        foreach (range(1,5) as $number) {
            $item=$loan->collaterals->values()->get($number-1);
            $values["collateral.$number.item"]=(string)($item?->item ?? '');
            $values["collateral.$number.code"]=(string)($item?->code ?? '');
            $values["collateral.$number.value"]=$item ? $money($item->accepted_value) : '';
            $values["collateral.$number.description"]=(string)($item?->description ?? '');
        }
        return $values;
    }

    private function applyDocxMappings(DOMDocument $document, DOMXPath $xpath, ContractTemplate $template, Loan $loan, array $overrides, array $formatting): void
    {
        $paragraphs=$xpath->query('/w:document/w:body//w:p');
        $values=$this->fieldValues($loan);
        foreach (array_values($template->mappings ?? []) as $index=>$mapping) {
            $field=(string)($mapping['field'] ?? '');
            if ($field==='' || ! array_key_exists($field,$values)) continue;
            $value=trim(array_key_exists($index,$overrides) ? (string)$overrides[$index] : (string)($values[$field] ?? ''));
            if ($value==='') continue;
            $blockId=(string)($mapping['block_id'] ?? '');
            if (! preg_match('/^p:(\d+)$/',$blockId,$match)) continue;
            $paragraph=$paragraphs?->item((int)$match[1]);
            if (! $paragraph instanceof DOMElement) continue;
            $texts=$xpath->query('.//w:t',$paragraph);
            if (! $texts || $texts->length===0) continue;
            $segment=(int)($mapping['segment_index'] ?? -1);
            if ($segment<0 || $segment>=$texts->length) $segment=$this->bestWritableSegment($texts);
            $target=$texts->item($segment);
            if (! $target instanceof DOMElement) continue;
            $format=$this->docxFormatFor($mapping,$formatting[$index] ?? []);
            if ($format['trim_surrounding']) $this->trimAdjacentRuns($texts,$segment);
            $this->writeValue($target,$value,(string)($mapping['mode'] ?? 'replace_blank'),$format['spaces_before'],$format['spaces_after'],$format['trim_surrounding']);
        }
    }

    private function docxFormatFor(array $mapping, array $override): array
    {
        $defaultBefore=(string)($mapping['mode'] ?? '')==='append' ? 1 : 0;
        $before=array_key_exists('spaces_before',$override) ? (int)$override['spaces_before'] : (int)($mapping['spaces_before'] ?? $defaultBefore);
        $after=array_key_exists('spaces_after',$override) ? (int)$override['spaces_after'] : (int)($mapping['spaces_after'] ?? 0);
        $trim=array_key_exists('trim_surrounding',$override) ? filter_var($override['trim_surrounding'],FILTER_VALIDATE_BOOLEAN) : (bool)($mapping['trim_surrounding'] ?? false);
        return ['spaces_before'=>max(0,min(10,$before)),'spaces_after'=>max(0,min(10,$after)),'trim_surrounding'=>$trim];
    }

    private function trimAdjacentRuns(\DOMNodeList $texts, int $segment): void
    {
        if ($segment>0 && ($previous=$texts->item($segment-1)) instanceof DOMElement) $previous->nodeValue=rtrim((string)$previous->nodeValue," \t");
        if ($segment+1<$texts->length && ($next=$texts->item($segment+1)) instanceof DOMElement) $next->nodeValue=ltrim((string)$next->nodeValue," \t");
    }

    private function writeValue(DOMElement $target, string $value, string $mode, int $before, int $after, bool $trim): void
    {
        $current=(string)$target->nodeValue;
        $wrapped=str_repeat(' ',$before).$value.str_repeat(' ',$after);
        if ($mode==='replace') $target->nodeValue=$wrapped;
        elseif ($mode==='append') $target->nodeValue=rtrim($current," \t").$wrapped;
        else {
            $pattern=$trim ? '/[ \t]*[.…·_]{3,}[ \t]*/u' : '/[.…·_]{3,}|\.{3,}/u';
            $replaced=preg_replace($pattern,$wrapped,$current,1,$count);
            $target->nodeValue=$count>0 && is_string($replaced) ? $replaced : rtrim($current," \t").$wrapped;
        }
        if (preg_match('/^\s|\s$/u',(string)$target->nodeValue)) $target->setAttributeNS('http://www.w3.org/XML/1998/namespace','xml:space','preserve');
    }

    private function bestWritableSegment(\DOMNodeList $texts): int
    {
        for ($i=0;$i<$texts->length;$i++) if (preg_match('/[.…·_]{3,}|\.{3,}/u',(string)$texts->item($i)?->nodeValue)) return $i;
        return max(0,$texts->length-1);
    }

    private function paragraphPreview(DOMElement $paragraph, DOMXPath $xpath): array
    {
        $text='';
        $nodes=$xpath->query('.//w:t',$paragraph);
        if ($nodes) foreach ($nodes as $node) $text.=(string)$node->nodeValue;
        $align=(string)($xpath->evaluate('string(./w:pPr/w:jc/@w:val)',$paragraph) ?: 'left');
        if (! in_array($align,['left','center','right','both'],true)) $align='left';
        return ['type'=>'paragraph','text'=>$text,'align'=>$align==='both'?'left':$align];
    }

    private function tablePreview(DOMElement $table, DOMXPath $xpath): array
    {
        $rows=[];
        $rowNodes=$xpath->query('./w:tr',$table);
        if ($rowNodes) foreach ($rowNodes as $row) {
            $cells=[];
            $cellNodes=$xpath->query('./w:tc',$row);
            if ($cellNodes) foreach ($cellNodes as $cell) {
                $paragraphs=[];
                $paragraphNodes=$xpath->query('./w:p',$cell);
                if ($paragraphNodes) foreach ($paragraphNodes as $paragraph) {
                    $text='';
                    $textNodes=$xpath->query('.//w:t',$paragraph);
                    if ($textNodes) foreach ($textNodes as $node) $text.=(string)$node->nodeValue;
                    $paragraphs[]=$text;
                }
                $cells[]=implode("\n",$paragraphs);
            }
            if ($cells!==[]) $rows[]=$cells;
        }
        return ['type'=>'table','rows'=>$rows];
    }

    private function assertZipAvailable(): void
    {
        if (! class_exists(ZipArchive::class)) throw ValidationException::withMessages(['contract'=>'DOCX contract editing requires the PHP Zip extension (php-zip).']);
    }

    private function documentXml(string $path): string
    {
        $this->assertZipAvailable();
        $zip=new ZipArchive();
        if ($zip->open($path)!==true) throw ValidationException::withMessages(['contract'=>'The contract template is not a valid DOCX package.']);
        try {
            $xml=$zip->getFromName('word/document.xml');
            if (! is_string($xml)) throw ValidationException::withMessages(['contract'=>'The DOCX template does not contain its main Word document.']);
            return $xml;
        } finally {
            $zip->close();
        }
    }

    private function dom(string $xml): array
    {
        $document=new DOMDocument('1.0','UTF-8');
        $document->preserveWhiteSpace=true;
        $document->formatOutput=false;
        $previous=libxml_use_internal_errors(true);
        $ok=$document->loadXML($xml,LIBXML_NONET | LIBXML_COMPACT);
        libxml_clear_errors();
        libxml_use_internal_errors($previous);
        if (! $ok) throw ValidationException::withMessages(['contract'=>'The Word document XML could not be parsed.']);
        $xpath=new DOMXPath($document);
        $xpath->registerNamespace('w','http://schemas.openxmlformats.org/wordprocessingml/2006/main');
        return [$document,$xpath];
    }
}
