<?php

namespace App\Services;

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

class DocxContractTemplateService
{
    public const MODES = [
        'replace_blank' => 'Replace dotted/blank area',
        'append' => 'Append value after existing label',
        'replace' => 'Replace selected text segment',
    ];

    /** @return array<string,string> */
    public function fieldCatalog(): array
    {
        $fields = [
            'business.name' => 'Business · Name',
            'business.phone' => 'Business · Phone',
            'business.email' => 'Business · Email',
            'business.address' => 'Business · Address',
            'business.registration_number' => 'Business · Registration number',
            'business.tin' => 'Business · TIN',
            'branch.name' => 'Branch · Name',
            'branch.phone' => 'Branch · Phone',
            'branch.address' => 'Branch · Address',
            'loan.number' => 'Loan · Loan number',
            'loan.principal' => 'Loan · Principal',
            'loan.interest' => 'Loan · Interest',
            'loan.processing_fee' => 'Loan · Processing fee',
            'loan.total_repayment' => 'Loan · Total repayment',
            'loan.installment_amount' => 'Loan · Scheduled installment amount',
            'loan.repayment_frequency' => 'Loan · Repayment frequency',
            'loan.term' => 'Loan · Duration / term',
            'loan.schedule_start_date' => 'Loan · Schedule start date',
            'loan.final_due_date' => 'Loan · Final due date',
            'loan.purpose' => 'Loan · Purpose',
            'customer.full_name' => 'Customer · Full name',
            'customer.phone' => 'Customer · Phone',
            'customer.nin' => 'Customer · NIN / NIDA',
            'customer.gender' => 'Customer · Gender',
            'customer.date_of_birth' => 'Customer · Date of birth',
            'customer.age' => 'Customer · Age',
            'customer.marital_status' => 'Customer · Marital status',
            'customer.occupation' => 'Customer · Occupation',
            'customer.employer_business' => 'Customer · Employer / business',
            'customer.monthly_income' => 'Customer · Monthly income',
            'customer.address' => 'Customer · Full address',
            'customer.region' => 'Customer · Region',
            'customer.district' => 'Customer · District',
            'customer.ward' => 'Customer · Ward',
            'customer.street_village' => 'Customer · Street / village',
            'guarantor.full_name' => 'Guarantor · Full name',
            'guarantor.phone' => 'Guarantor · Phone',
            'guarantor.nin' => 'Guarantor · NIN / NIDA',
            'guarantor.address' => 'Guarantor · Address',
            'guarantor.occupation' => 'Guarantor · Occupation',
            'guarantor.relationship' => 'Guarantor · Relationship to borrower',
            'loan_officer.name' => 'Staff · Loan officer / application creator',
            'approver.name' => 'Staff · Approver',
            'collateral.summary' => 'Collateral · Summary of all pledged items',
        ];

        for ($i = 1; $i <= 5; $i++) {
            $fields["collateral.$i.item"] = "Collateral $i · Item";
            $fields["collateral.$i.code"] = "Collateral $i · Code";
            $fields["collateral.$i.value"] = "Collateral $i · Accepted value";
            $fields["collateral.$i.description"] = "Collateral $i · Description";
        }

        return $fields;
    }

    /** @return array{extracted_text:string,blocks:array<int,array<string,mixed>>,mappings:array<int,array<string,mixed>>} */
    public function inspectPath(string $absolutePath): array
    {
        $xml = $this->documentXml($absolutePath);
        [$document, $xpath] = $this->dom($xml);
        $blocks = $this->blocks($xpath);
        $mappings = $this->autoMappings($blocks);

        return [
            'extracted_text' => collect($blocks)->pluck('text')->filter()->implode("\n"),
            'blocks' => $blocks,
            'mappings' => $mappings,
        ];
    }

    public function generate(ContractTemplate $template, Loan $loan): string
    {
        $this->assertZipAvailable();
        $loan->loadMissing([
            'customer.address','branch.business','branch.manager','product','guarantor','collaterals.category',
            'installments','creator','approver',
        ]);

        $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-');
        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);
            $paragraphs = $xpath->query('/w:document/w:body//w:p');
            $values = $this->values($loan);

            foreach (($template->mappings ?? []) as $mapping) {
                $field = (string) ($mapping['field'] ?? '');
                if ($field === '' || ! array_key_exists($field, $values)) continue;
                $value = trim((string) ($values[$field] ?? ''));
                if ($value === '') continue; // Unknown values remain blank for manual completion.

                $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 DOMNode) continue;

                $this->writeValue($target, $value, (string) ($mapping['mode'] ?? 'replace_blank'));
            }

            $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;
    }

    private function assertZipAvailable(): void
    {
        if (! class_exists(ZipArchive::class)) {
            throw ValidationException::withMessages([
                'template_file' => 'DOCX contract templates require 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(['template_file' => 'The uploaded file is not a valid DOCX package.']);
        }
        try {
            $xml = $zip->getFromName('word/document.xml');
            if (! is_string($xml)) {
                throw ValidationException::withMessages(['template_file' => 'The DOCX file is missing its main Word document content.']);
            }
            return $xml;
        } finally {
            $zip->close();
        }
    }

    /** @return array{0:DOMDocument,1:DOMXPath} */
    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(['template_file' => '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];
    }

    /** @return array<int,array<string,mixed>> */
    private function blocks(DOMXPath $xpath): array
    {
        $nodes = $xpath->query('/w:document/w:body//w:p');
        $blocks = [];
        if (! $nodes) return $blocks;

        foreach ($nodes as $index => $paragraph) {
            $segments = [];
            $textNodes = $xpath->query('.//w:t', $paragraph);
            if ($textNodes) {
                foreach ($textNodes as $segmentIndex => $textNode) {
                    $segments[] = ['index' => $segmentIndex, 'text' => (string) $textNode->nodeValue];
                }
            }
            $text = collect($segments)->pluck('text')->implode('');
            if (trim($text) === '') continue;
            $blocks[] = [
                'id' => 'p:'.$index,
                'text' => $text,
                'segments' => $segments,
            ];
        }
        return $blocks;
    }

    /** @param array<int,array<string,mixed>> $blocks @return array<int,array<string,mixed>> */
    private function autoMappings(array $blocks): array
    {
        $rules = $this->rules();
        $mappings = [];
        $seen = [];
        $context = 'general';

        foreach ($blocks as $block) {
            $text = (string) $block['text'];
            $normalized = $this->normalize($text);
            if ($normalized === '') continue;

            if (str_contains($normalized, 'taarifa binafsi za mwombaji') || str_contains($normalized, 'taarifa za mteja')) $context = 'customer';
            if (str_contains($normalized, 'taarifa za mdhamini')) $context = 'guarantor';
            if (str_contains($normalized, 'masharti mengineyo')) $context = 'general';

            foreach ($rules as $rule) {
                if (! in_array($context, $rule['contexts'], true)) continue;
                if (! preg_match($rule['pattern'], $text, $match, PREG_OFFSET_CAPTURE)) continue;

                $matchedText = (string) $match[0][0];
                $offset = (int) $match[0][1];
                $target = $this->targetSegment($block['segments'] ?? [], $offset + strlen($matchedText));
                if ($target === null) continue;

                $key = $block['id'].'|'.$target['index'].'|'.$rule['field'];
                if (isset($seen[$key])) continue;
                $seen[$key] = true;
                $mappings[] = [
                    'block_id' => $block['id'],
                    'segment_index' => $target['index'],
                    'field' => $rule['field'],
                    'mode' => $target['has_blank'] ? 'replace_blank' : 'append',
                    'label' => $rule['label'],
                    'source_text' => mb_substr($text, 0, 220),
                ];
            }
        }

        return $mappings;
    }

    /** @return array<int,array{pattern:string,field:string,label:string,contexts:array<int,string>}> */
    private function rules(): array
    {
        $all = ['general','customer','guarantor'];
        return [
            ['pattern'=>'/JINA\s+LA\s+MTEJA/iu','field'=>'customer.full_name','label'=>'Jina la mteja','contexts'=>['customer','general']],
            ['pattern'=>'/Jina\s+la\s+mteja/iu','field'=>'customer.full_name','label'=>'Jina la mteja','contexts'=>['customer','general']],
            ['pattern'=>'/Mimi\s+[.…._]{3,}/iu','field'=>'customer.full_name','label'=>'Mimi (borrower)','contexts'=>['customer']],
            ['pattern'=>'/Kazi\s+ya\s+mteja\s*\/\s*Biashara/iu','field'=>'customer.occupation','label'=>'Kazi/Biashara','contexts'=>['customer']],
            ['pattern'=>'/Umri\s+wa\s+mteja/iu','field'=>'customer.age','label'=>'Umri wa mteja','contexts'=>['customer']],
            ['pattern'=>'/Jinsia/iu','field'=>'customer.gender','label'=>'Jinsia','contexts'=>['customer']],
            ['pattern'=>'/Namba\s+ya\s+nida/iu','field'=>'customer.nin','label'=>'Namba ya NIDA','contexts'=>['customer']],
            ['pattern'=>'/Hali\s+ya\s+ndoa/iu','field'=>'customer.marital_status','label'=>'Hali ya ndoa','contexts'=>['customer']],
            ['pattern'=>'/Namba\s+ya\s+simu/iu','field'=>'customer.phone','label'=>'Namba ya simu','contexts'=>['customer']],
            ['pattern'=>'/ANAPOISHI/iu','field'=>'customer.address','label'=>'Anapoishi','contexts'=>['customer']],
            ['pattern'=>'/MAWASILIANO/iu','field'=>'customer.phone','label'=>'Mawasiliano','contexts'=>['customer']],
            ['pattern'=>'/JINA\s+LA\s+MDHAMINI/iu','field'=>'guarantor.full_name','label'=>'Jina la mdhamini','contexts'=>['guarantor','general']],
            ['pattern'=>'/Mimi\s+[.…._]{3,}/iu','field'=>'guarantor.full_name','label'=>'Mimi (guarantor)','contexts'=>['guarantor']],
            ['pattern'=>'/uhusiano/iu','field'=>'guarantor.relationship','label'=>'Uhusiano','contexts'=>['guarantor']],
            ['pattern'=>'/namba\s+ya\s+simu/iu','field'=>'guarantor.phone','label'=>'Namba ya simu ya mdhamini','contexts'=>['guarantor']],
            ['pattern'=>'/Jina\s+la\s+afisa\s+aliyekopesha/iu','field'=>'loan_officer.name','label'=>'Afisa aliyekopesha','contexts'=>$all],
            ['pattern'=>'/Tarehe\s+ya\s+kukopesha/iu','field'=>'loan.schedule_start_date','label'=>'Tarehe ya kukopesha','contexts'=>$all],
            ['pattern'=>'/kiasi\s+cha\s+Tsh/iu','field'=>'loan.principal','label'=>'Kiasi cha mkopo','contexts'=>['customer','general']],
            ['pattern'=>'/jumla\s+ya\s+fedha\s+ninayopaswa\s+kulipa/iu','field'=>'loan.total_repayment','label'=>'Jumla ya kurejesha','contexts'=>['customer','general']],
            ['pattern'=>'/kila\s+siku\s*\/\s*wiki\s*\/\s*mwezi\s*Tsh/iu','field'=>'loan.installment_amount','label'=>'Kiasi cha kila rejesho','contexts'=>['customer','general']],
        ];
    }

    /** @param array<int,array{index:int,text:string}> $segments @return array{index:int,has_blank:bool}|null */
    private function targetSegment(array $segments, int $afterOffset): ?array
    {
        $cursor = 0;
        $fallback = null;
        foreach ($segments as $segment) {
            $text = (string) ($segment['text'] ?? '');
            $start = $cursor;
            $end = $cursor + strlen($text);
            $cursor = $end;
            if ($end < $afterOffset) continue;
            if ($fallback === null) $fallback = (int) $segment['index'];
            if (preg_match('/[.…·_]{3,}|\.{3,}/u', $text)) {
                return ['index'=>(int)$segment['index'],'has_blank'=>true];
            }
            if ($start > $afterOffset + 180) break;
        }
        if ($fallback !== null) return ['index'=>$fallback,'has_blank'=>false];
        if ($segments !== []) return ['index'=>(int)end($segments)['index'],'has_blank'=>false];
        return null;
    }

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

    private function writeValue(DOMNode $target, string $value, string $mode): void
    {
        $current = (string) $target->nodeValue;
        if ($mode === 'replace') {
            $target->nodeValue = $value;
            return;
        }
        if ($mode === 'append') {
            $separator = $current !== '' && ! preg_match('/\s$/u', $current) ? ' ' : '';
            $target->nodeValue = $current.$separator.$value;
            return;
        }
        $replaced = preg_replace('/[.…·_]{3,}|\.{3,}/u', $value, $current, 1, $count);
        if ($count > 0 && is_string($replaced)) {
            $target->nodeValue = $replaced;
            return;
        }
        $separator = $current !== '' && ! preg_match('/\s$/u', $current) ? ' ' : '';
        $target->nodeValue = $current.$separator.$value;
    }

    /** @return array<string,string> */
    private function values(Loan $loan): array
    {
        $business = $loan->branch->business;
        $customer = $loan->customer;
        $address = $customer->address;
        $guarantor = $loan->guarantor;
        $firstInstallment = $loan->installments->first();
        $lastInstallment = $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' => $firstInstallment ? $money($firstInstallment->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' => $lastInstallment?->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 normalize(string $text): string
    {
        return mb_strtolower(trim(preg_replace('/\s+/u', ' ', $text) ?? $text));
    }
}
