<?php

namespace App\Http\Controllers;

use App\Models\Branch;
use App\Models\Collateral;
use App\Models\ContractTemplate;
use App\Models\Customer;
use App\Models\Guarantor;
use App\Models\Loan;
use App\Models\LoanDocument;
use App\Models\LoanProduct;
use App\Services\AuditService;
use App\Services\DocxContractTemplateService;
use App\Services\LoanApplicationService;
use App\Services\LoanApprovalService;
use App\Services\LoanContractPdfService;
use App\Services\RepaymentScheduleGenerator;
use App\Services\SecureFileService;
use Carbon\CarbonImmutable;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
use InvalidArgumentException;
use Symfony\Component\HttpFoundation\StreamedResponse;

class LoanApplicationController extends Controller
{
    public function index(Request $request): View
    {
        $query = Loan::with(['customer','branch','product'])->whereIn('branch_id', $this->allowedBranchIds($request))->latest('id');
        if ($request->filled('search')) {
            $search = trim((string)$request->string('search'));
            $query->where(function ($q) use ($search): void {
                $q->where('loan_number','like',"%{$search}%")
                    ->orWhereHas('customer', fn($customer) => $customer->where('customer_number','like',"%{$search}%")
                        ->orWhere('first_name','like',"%{$search}%")->orWhere('middle_name','like',"%{$search}%")
                        ->orWhere('last_name','like',"%{$search}%")->orWhere('phone','like',"%{$search}%"));
            });
        }
        if ($request->filled('status')) $query->where('status',$request->string('status'));
        if ($request->filled('product')) $query->where('loan_product_id',$request->integer('product'));
        if ($request->filled('branch')) {
            $branchId=$request->integer('branch');
            $this->assertBranchAccess($request,$branchId);
            $query->where('branch_id',$branchId);
        }

        return view('loans.index', [
            'loans'=>$query->paginate(20)->withQueryString(),
            'products'=>$this->availableProducts($request),
            'branches'=>Branch::whereIn('id',$this->allowedBranchIds($request))->orderBy('name')->get(),
            'statuses'=>Loan::STATUSES,
        ]);
    }

    public function create(Request $request): View
    {
        return view('loans.form', $this->formData($request, new Loan(['schedule_start_date'=>now(),'status'=>'draft'])));
    }

    public function store(Request $request, LoanApplicationService $service, SecureFileService $files): RedirectResponse
    {
        $data = $this->validated($request);
        [$product] = $this->assertReferences($request, $data);
        $loan = $service->create($data, $product, $request->user());
        $this->storeSupportingDocuments($request, $loan, $files);
        AuditService::record('loan_application.created',$loan,[],$loan->fresh()->toArray(),$loan->branch_id);
        return redirect()->route('loans.show',$loan)->with('success','Loan application created as draft.');
    }

    public function show(Request $request, Loan $loan, LoanApprovalService $approval): View
    {
        $this->assertBranchAccess($request,$loan->branch_id);
        $loan->load(['customer','branch.business','product','contractTemplate','guarantor','collaterals.category','installments','documents.uploader','statusHistory.changer','approvalDecisions.user','creator','approver']);
        return view('loans.show', [
            'loan'=>$loan,
            'transitions'=>Loan::TRANSITIONS[$loan->status] ?? [],
            'requireSignedContract'=>(bool)$loan->branch->business->require_signed_contract_before_disbursement,
            'requiresManagerApproval'=>$approval->requiresManagerApproval($loan),
            'makerCheckerBlocked'=>$approval->makerCheckerBlocks($loan,$request->user()),
            'availableAdditionalCollateral'=>Collateral::with('category')->where('branch_id',$loan->branch_id)->where('customer_id',$loan->customer_id)
                ->whereNotIn('status',['released','sold','returned'])->whereNotIn('id',$loan->collaterals->pluck('id'))->orderBy('code')->get(),
        ]);
    }

    public function edit(Request $request, Loan $loan): View
    {
        $this->assertBranchAccess($request,$loan->branch_id);
        abort_unless($loan->canEditCoreTerms(), 422, 'Core loan terms can only be edited while the loan is a draft or correction has been requested.');
        return view('loans.form', $this->formData($request,$loan->load(['collaterals'])));
    }

    public function update(Request $request, Loan $loan, LoanApplicationService $service): RedirectResponse
    {
        $this->assertBranchAccess($request,$loan->branch_id);
        abort_unless($loan->canEditCoreTerms(), 422, 'Core loan terms can only be edited while the loan is a draft or correction has been requested.');
        $data = $this->validated($request,$loan);
        $data['branch_id']=$loan->branch_id;
        $data['customer_id']=$loan->customer_id;
        [$product] = $this->assertReferences($request,$data);
        $before=$loan->toArray();
        $service->updateDraft($loan,$data,$product,$request->user());
        AuditService::record('loan_application.updated',$loan,$before,$loan->fresh()->toArray(),$loan->branch_id);
        return redirect()->route('loans.show',$loan)->with('success','Loan application updated.');
    }

    public function preview(Request $request, RepaymentScheduleGenerator $generator): JsonResponse
    {
        $data=$request->validate([
            'loan_product_id'=>['required','integer','exists:loan_products,id'],
            'principal'=>['required','decimal:0,2','min:0.01'],
            'schedule_start_date'=>['required','date'],
            'custom_dates_text'=>['nullable','string','max:5000'],
        ]);
        $product=LoanProduct::findOrFail($data['loan_product_id']);
        abort_unless($this->availableProducts($request)->contains('id',$product->id),403);
        try {
            $result=$generator->generate($product,(string)$data['principal'],CarbonImmutable::parse($data['schedule_start_date']),$this->customDates($data['custom_dates_text'] ?? null));
        } catch (InvalidArgumentException $e) {
            return response()->json(['message'=>$e->getMessage()],422);
        }
        return response()->json($result);
    }

    public function transition(Request $request, Loan $loan, LoanApplicationService $service, LoanApprovalService $approval): RedirectResponse
    {
        $this->assertBranchAccess($request,$loan->branch_id);
        $data=$request->validate(['status'=>['required',Rule::in(Loan::STATUSES)],'notes'=>['nullable','string','max:2000']]);
        $this->assertStoredRequirements($loan);
        $before=$loan->status;
        $updated=$data['status']==='under_review'
            ? $approval->submit($loan,$request->user(),$data['notes'] ?? null)
            : $service->transition($loan,$data['status'],$request->user(),$data['notes'] ?? null);
        $action=$updated->status==='ready_for_disbursement' ? 'loan_approval.auto_approved' : 'loan_application.status_changed';
        AuditService::record($action,$loan,['status'=>$before],['status'=>$updated->status],$loan->branch_id);
        return back()->with('success',$updated->status==='ready_for_disbursement'?'Loan automatically approved and is ready for disbursement.':'Loan application status updated.');
    }

    public function contract(
        Request $request,
        Loan $loan,
        LoanApplicationService $workflow,
        LoanContractPdfService $pdf,
        DocxContractTemplateService $docx,
    ): Response {
        $this->assertBranchAccess($request,$loan->branch_id);
        abort_unless($loan->canGenerateContract(),422,'A new contract cannot be generated while the application is under review or after it has been approved/rejected.');
        $this->assertStoredRequirements($loan);
        $loan->loadMissing(['branch.business','contractTemplate']);

        $template = $loan->contractTemplate;
        if (! $template) {
            $template = ContractTemplate::query()
                ->where('business_id',$loan->branch->business_id)
                ->where('status','active')
                ->where('is_default',true)
                ->first();
        }

        if ($template) {
            $content = $docx->generate($template,$loan);
            $filename = $loan->loan_number.'-contract.docx';
            $path = 'loans/'.$loan->id.'/generated-contracts/'.now()->format('Ymd-His').'-'.$filename;
            Storage::disk('local')->put($path,$content);

            if ($loan->contract_template_id !== $template->id) {
                $loan->update(['contract_template_id'=>$template->id,'updated_by'=>$request->user()->id]);
            }
            $workflow->markContractGenerated($loan->fresh(),$request->user());
            $document = LoanDocument::create([
                'loan_id'=>$loan->id,
                'kind'=>'generated_contract',
                'document_type'=>'Generated Loan Contract',
                'original_name'=>$filename,
                'file_path'=>$path,
                'mime_type'=>'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
                'file_size'=>strlen($content),
                'notes'=>'Generated from contract template '.$template->name.' v'.$template->version.'.',
                'uploaded_by'=>$request->user()->id,
            ]);

            AuditService::record('loan_contract.generated',$document,[],[
                'loan_id'=>$loan->id,'contract_template_id'=>$template->id,'template_name'=>$template->name,
                'contract_generated_at'=>$loan->fresh()->contract_generated_at,
            ],$loan->branch_id);

            return response($content,200,[
                'Content-Type'=>'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
                'Content-Disposition'=>'attachment; filename="'.$filename.'"',
                'Content-Length'=>(string)strlen($content),
                'Cache-Control'=>'private, no-store, max-age=0',
            ]);
        }

        // Backward-compatible fallback until the business uploads and activates its own DOCX template.
        $content=$pdf->generate($loan->fresh(),$loan->branch->business);
        $workflow->markContractGenerated($loan,$request->user());
        AuditService::record('loan_contract.generated',$loan,[],['contract_generated_at'=>$loan->fresh()->contract_generated_at,'fallback'=>'generic_pdf'],$loan->branch_id);
        return response($content,200,[
            'Content-Type'=>'application/pdf',
            'Content-Disposition'=>'attachment; filename="'.$loan->loan_number.'-contract.pdf"',
            'Content-Length'=>(string)strlen($content),
        ]);
    }

    public function uploadSignedContract(Request $request, Loan $loan, SecureFileService $files, LoanApplicationService $workflow): RedirectResponse
    {
        $this->assertBranchAccess($request,$loan->branch_id);
        abort_unless($loan->contract_generated_at,422,'Generate the loan contract before uploading a signed copy.');
        abort_unless(
            $loan->canUploadSignedContract(),
            422,
            'Signed contract uploads are locked while the application is under review or after approval/rejection. They reopen only when a reviewer requests correction, documents, or additional collateral.'
        );
        $data=$request->validate(['signed_contract'=>['required','file','mimes:pdf,jpg,jpeg,png,doc,docx','max:15360'],'notes'=>['nullable','string','max:1000']]);
        $file=$data['signed_contract'];
        $path=$files->store($file,'loans/'.$loan->id.'/signed-contracts');
        $document=LoanDocument::create([
            'loan_id'=>$loan->id,'kind'=>'signed_contract','document_type'=>'Signed Loan Contract','original_name'=>$file->getClientOriginalName(),
            'file_path'=>$path,'mime_type'=>$file->getMimeType(),'file_size'=>$file->getSize(),'notes'=>$data['notes'] ?? null,'uploaded_by'=>$request->user()->id,
        ]);
        $workflow->markContractSigned($loan,$request->user());
        AuditService::record('loan_contract.signed_uploaded',$document,[],$document->toArray(),$loan->branch_id);
        return back()->with('success','Signed loan contract uploaded.');
    }

    public function uploadDocument(Request $request, Loan $loan, SecureFileService $files): RedirectResponse
    {
        $this->assertBranchAccess($request,$loan->branch_id);
        abort_unless(
            $loan->canUploadSupportingDocument(),
            422,
            'Application documents are read-only while the loan is under review or after approval/rejection. A reviewer must request correction, documents, or additional collateral before new files can be added.'
        );
        $data=$request->validate([
            'document_type'=>['required','string','max:150'],'attachment'=>['required','file','mimes:pdf,jpg,jpeg,png,webp,doc,docx','max:10240'],'notes'=>['nullable','string','max:1000'],
        ]);
        $file=$data['attachment'];
        $path=$files->store($file,'loans/'.$loan->id.'/supporting');
        $document=LoanDocument::create([
            'loan_id'=>$loan->id,'kind'=>'supporting','document_type'=>$data['document_type'],'original_name'=>$file->getClientOriginalName(),
            'file_path'=>$path,'mime_type'=>$file->getMimeType(),'file_size'=>$file->getSize(),'notes'=>$data['notes'] ?? null,'uploaded_by'=>$request->user()->id,
        ]);
        AuditService::record('loan_document.uploaded',$document,[],$document->toArray(),$loan->branch_id);
        return back()->with('success','Loan supporting document uploaded.');
    }

    public function downloadDocument(Request $request, Loan $loan, LoanDocument $document, SecureFileService $files): StreamedResponse
    {
        $this->assertBranchAccess($request,$loan->branch_id);
        abort_unless($document->loan_id === $loan->id,404);
        return $files->download($document->file_path,$document->original_name);
    }

    private function validated(Request $request, ?Loan $loan=null): array
    {
        $data=$request->validate([
            'branch_id'=>[$loan?'nullable':'required','integer','exists:branches,id'],
            'customer_id'=>[$loan?'nullable':'required','integer','exists:customers,id'],
            'loan_product_id'=>['required','integer','exists:loan_products,id'],
            'principal'=>['required','decimal:0,2','min:0.01'],
            'purpose'=>['required','string','max:4000'],
            'schedule_start_date'=>['required','date'],
            'custom_dates_text'=>['nullable','string','max:5000'],
            'collateral_ids'=>['nullable','array'],'collateral_ids.*'=>['integer','exists:collaterals,id'],
            'guarantor_id'=>['nullable','integer','exists:guarantors,id'],
            'supporting_documents'=>['nullable','array'],'supporting_documents.*'=>['file','mimes:pdf,jpg,jpeg,png,webp,doc,docx','max:10240'],
        ]);
        $data['custom_repayment_dates']=$this->customDates($data['custom_dates_text'] ?? null);
        unset($data['custom_dates_text'],$data['supporting_documents']);
        return $data;
    }

    private function assertReferences(Request $request, array $data): array
    {
        $this->assertBranchAccess($request,(int)$data['branch_id']);
        $branch=Branch::findOrFail($data['branch_id']);
        $customer=Customer::findOrFail($data['customer_id']);
        $product=LoanProduct::findOrFail($data['loan_product_id']);
        abort_unless($customer->branch_id === $branch->id,422,'Customer must belong to the selected branch.');
        abort_unless($product->business_id === $branch->business_id && $product->status === 'active',422,'Loan product is not available for this branch.');

        $collateralIds=array_map('intval',$data['collateral_ids'] ?? []);
        $validCollateral=Collateral::whereIn('id',$collateralIds)->where('customer_id',$customer->id)->where('branch_id',$branch->id)
            ->whereNotIn('status',['released','sold','returned'])->count();
        abort_unless($validCollateral === count($collateralIds),422,'All selected collateral must belong to the customer and selected branch and remain available.');
        if ($product->collateral_required && empty($collateralIds)) throw ValidationException::withMessages(['collateral_ids'=>'This loan product requires collateral.']);

        if (!empty($data['guarantor_id'])) {
            $guarantor=Guarantor::findOrFail($data['guarantor_id']);
            abort_unless($guarantor->customer_id === $customer->id && $guarantor->status === 'active',422,'Guarantor must be an active guarantor for this customer.');
        } elseif ($product->guarantor_required) {
            throw ValidationException::withMessages(['guarantor_id'=>'This loan product requires a guarantor.']);
        }

        return [$product,$branch,$customer];
    }

    private function assertStoredRequirements(Loan $loan): void
    {
        $loan->loadMissing(['product','collaterals','guarantor']);
        if ($loan->product->collateral_required && $loan->collaterals->isEmpty()) throw ValidationException::withMessages(['collateral'=>'Required collateral is missing.']);
        if ($loan->product->guarantor_required && !$loan->guarantor) throw ValidationException::withMessages(['guarantor'=>'Required guarantor is missing.']);
    }

    private function formData(Request $request, Loan $loan): array
    {
        $branchIds=$this->allowedBranchIds($request);
        $businessIds=Branch::whereIn('id',$branchIds)->pluck('business_id')->unique();
        return [
            'loan'=>$loan,
            'branches'=>Branch::whereIn('id',$branchIds)->where('status','active')->orderBy('name')->get(),
            'customers'=>Customer::whereIn('branch_id',$branchIds)->where('status','active')->orderBy('first_name')->orderBy('last_name')->get(),
            'products'=>LoanProduct::whereIn('business_id',$businessIds)->where('status','active')->orderBy('name')->get(),
            'collaterals'=>Collateral::with('category')->whereIn('branch_id',$branchIds)->whereNotIn('status',['released','sold','returned'])->orderBy('code')->get(),
            'guarantors'=>Guarantor::with('customer')->whereHas('customer',fn($q)=>$q->whereIn('branch_id',$branchIds))->where('status','active')->orderBy('full_name')->get(),
        ];
    }

    private function availableProducts(Request $request)
    {
        $businessIds=Branch::whereIn('id',$this->allowedBranchIds($request))->pluck('business_id')->unique();
        return LoanProduct::whereIn('business_id',$businessIds)->where('status','active')->orderBy('name')->get();
    }

    private function allowedBranchIds(Request $request)
    {
        return collect(app(\App\Services\BranchAccessService::class)->accessibleBranchIds($request->user()));
    }

    private function assertBranchAccess(Request $request, int $branchId): void
    {
        abort_unless($request->user()->canAccessBranch($branchId),403);
    }

    private function customDates(?string $text): array
    {
        if (blank($text)) return [];
        $dates=collect(preg_split('/[\r\n,]+/',$text))->map(fn($date)=>trim($date))->filter()->values()->all();
        foreach ($dates as $date) {
            try { CarbonImmutable::parse($date); } catch (\Throwable) { throw ValidationException::withMessages(['custom_dates_text'=>"Invalid custom repayment date: {$date}"]); }
        }
        return $dates;
    }

    private function storeSupportingDocuments(Request $request, Loan $loan, SecureFileService $files): void
    {
        foreach ($request->file('supporting_documents',[]) as $file) {
            $path=$files->store($file,'loans/'.$loan->id.'/supporting');
            LoanDocument::create([
                'loan_id'=>$loan->id,'kind'=>'supporting','document_type'=>'Application Supporting Document','original_name'=>$file->getClientOriginalName(),
                'file_path'=>$path,'mime_type'=>$file->getMimeType(),'file_size'=>$file->getSize(),'uploaded_by'=>$request->user()->id,
            ]);
        }
    }
}
