<?php

namespace App\Services;

use App\Models\DamageFinding;
use App\Models\Inspection;
use App\Models\InspectionCollection;
use App\Models\InspectionWorkspace;
use App\Models\InspectionReport;
use App\Models\User;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;

class InspectionScanRecorder
{
    private const MAX_STORABLE_COST = 9_999_999_999;

    public function record(User $user, array $results, array $sourceImages, array $settings = []): Inspection
    {
        $collection = $this->resolveCollection($user, $settings);
        $currency = $this->normalizeCurrency($settings['currency'] ?? 'IDR');
        $sourceImagePaths = $this->storeSourceImages($sourceImages);
        $visualizationImages = collect($results)
            ->flatMap(fn (array $result) => $result['output_images'] ?? [])
            ->values()
            ->all();
        $outputKeys = collect($results)
            ->flatMap(fn (array $result) => $result['output_keys'] ?? [])
            ->unique()
            ->values()
            ->all();
        $findings = $this->extractFindings($results);
        $estimatedCost = $this->estimateRepairCost($findings);
        $averageConfidence = collect($findings)->avg('confidence') ?? 0;

        $inspection = Inspection::query()->create([
            'user_id' => $user->id,
            'inspection_collection_id' => $collection?->id,
            'property_name' => $collection?->displayName() ?? 'Roboflow Scan '.now()->format('M d, Y H:i'),
            'property_address' => $collection?->property_address ?? 'Uploaded from New Inspection',
            'status' => 'completed',
            'ai_confidence' => round((float) $averageConfidence, 2),
            'estimated_cost' => $estimatedCost,
            'currency_code' => $currency,
            'estimated_cost_label' => $this->formatMoney($estimatedCost, $currency),
            'scheduled_at' => now(),
            'completed_at' => now(),
            'source_image_paths' => $sourceImagePaths,
            'visualization_image_paths' => $visualizationImages,
            'roboflow_output_keys' => $outputKeys,
            'roboflow_outputs' => $results,
            'analysis_settings' => $settings,
        ]);

        foreach ($findings as $finding) {
            DamageFinding::query()->create([
                'inspection_id' => $inspection->id,
                'category' => $finding['category'],
                'severity' => $finding['severity'],
                'status' => 'review',
                'confidence' => $finding['confidence'],
                'estimated_repair_cost' => $finding['estimated_repair_cost'],
                'notes' => $finding['notes'],
            ]);
        }

        InspectionReport::query()->create([
            'inspection_id' => $inspection->id,
            'report_number' => 'RPT-RF-'.now()->format('Ymd').'-'.str_pad((string) $inspection->id, 4, '0', STR_PAD_LEFT),
            'status' => 'ready',
            'summary' => $this->buildSummary($inspection, $results, $findings),
            'published_at' => now(),
        ]);

        return $inspection->load(['damageFindings', 'report']);
    }

    private function resolveCollection(User $user, array $settings): ?InspectionCollection
    {
        $workspace = $this->resolveWorkspace($user, $settings);
        $propertyName = trim((string) ($settings['property_name'] ?? ''));
        $roomName = trim((string) ($settings['room_name'] ?? ''));
        $collectionName = trim((string) ($settings['collection_name'] ?? ''));
        $address = trim((string) ($settings['property_address'] ?? ''));
        $type = (string) ($settings['collection_type'] ?? ($roomName !== '' ? 'room' : 'house'));

        if (! empty($settings['collection_id'])) {
            $collection = InspectionCollection::query()
                ->where('user_id', $user->id)
                ->where('inspection_workspace_id', $workspace->id)
                ->find($settings['collection_id']);

            if ($collection !== null) {
                return $collection;
            }
        }

        if ($propertyName === '' && $collectionName === '' && $roomName === '') {
            $propertyName = 'General Collection';
        }

        if ($propertyName === '') {
            $propertyName = $collectionName !== '' ? $collectionName : 'Uploaded Property';
        }

        $name = $collectionName !== ''
            ? $collectionName
            : ($roomName !== '' ? "{$propertyName} / {$roomName}" : $propertyName);

        return InspectionCollection::query()->firstOrCreate(
            [
                'user_id' => $user->id,
                'inspection_workspace_id' => $workspace->id,
                'property_name' => $propertyName,
                'room_name' => $roomName !== '' ? $roomName : null,
            ],
            [
                'name' => $name,
                'collection_type' => in_array($type, ['house', 'room', 'collection'], true) ? $type : 'room',
                'property_address' => $address !== '' ? $address : null,
                'metadata' => [
                    'created_from' => 'scan',
                ],
            ]
        );
    }

    private function resolveWorkspace(User $user, array $settings): InspectionWorkspace
    {
        if (! empty($settings['workspace_id'])) {
            $workspace = InspectionWorkspace::query()
                ->where('user_id', $user->id)
                ->find($settings['workspace_id']);

            if ($workspace !== null) {
                return $workspace;
            }
        }

        $workspaceName = trim((string) ($settings['workspace_name'] ?? ''));

        return InspectionWorkspace::query()->firstOrCreate(
            [
                'user_id' => $user->id,
                'name' => $workspaceName !== '' ? $workspaceName : 'Default Workspace',
            ],
            [
                'default_currency' => $this->normalizeCurrency($settings['currency'] ?? 'IDR'),
                'metadata' => ['created_from' => 'scan'],
            ]
        );
    }

    public function attachGroqAnalyses(Inspection $inspection, array $analyses, array $settings = []): Inspection
    {
        $aiFindings = $this->extractGroqFindings($analyses, $settings);

        if ($aiFindings !== []) {
            $inspection->damageFindings()->delete();

            foreach ($aiFindings as $finding) {
                DamageFinding::query()->create([
                    'inspection_id' => $inspection->id,
                    'category' => $finding['category'],
                    'severity' => $finding['severity'],
                    'status' => 'review',
                    'confidence' => $finding['confidence'],
                    'estimated_repair_cost' => $finding['estimated_repair_cost'],
                    'notes' => $finding['notes'],
                ]);
            }
        }

        $estimatedCost = $aiFindings !== [] ? $this->estimateRepairCost($aiFindings) : (int) $inspection->estimated_cost;
        $currency = $this->normalizeCurrency($settings['currency'] ?? $inspection->currency_code ?? 'IDR');

        $inspection->forceFill([
            'groq_analysis_outputs' => $this->normalizeAnalysisOutputs($analyses, $currency),
            'analysis_settings' => $settings,
            'ai_confidence' => round((float) (collect($aiFindings)->avg('confidence') ?? $inspection->ai_confidence), 2),
            'estimated_cost' => $estimatedCost,
            'currency_code' => $currency,
            'estimated_cost_label' => $this->formatMoney($estimatedCost, $currency),
        ])->save();

        $inspection->report?->forceFill([
            'summary' => $this->buildAdvancedSummary($inspection->fresh(['damageFindings']), $analyses),
            'status' => 'ready',
            'published_at' => now(),
        ])->save();

        return $inspection->fresh(['damageFindings', 'report']);
    }

    private function storeSourceImages(array $sourceImages): array
    {
        return collect($sourceImages)
            ->filter(fn ($image) => $image instanceof UploadedFile)
            ->map(function (UploadedFile $image) {
                $path = $image->store('inspections/source/'.now()->format('Y/m/d'), 'public');

                return [
                    'name' => $image->getClientOriginalName(),
                    'path' => $path,
                    'url' => '/storage/'.$path,
                ];
            })
            ->values()
            ->all();
    }

    private function extractFindings(array $results): array
    {
        $predictionSets = [];

        foreach ($results as $result) {
            $this->collectPredictionSets($result['outputs'] ?? [], $predictionSets);
        }

        return collect($predictionSets)
            ->flatten(1)
            ->filter(fn ($prediction) => is_array($prediction))
            ->map(fn (array $prediction) => $this->predictionToFinding($prediction))
            ->values()
            ->all();
    }

    private function collectPredictionSets(mixed $value, array &$sets): void
    {
        if (! is_array($value)) {
            return;
        }

        if ($this->looksLikePredictionList($value)) {
            $sets[] = $value;

            return;
        }

        foreach ($value as $item) {
            $this->collectPredictionSets($item, $sets);
        }
    }

    private function looksLikePredictionList(array $value): bool
    {
        if (! array_is_list($value) || $value === []) {
            return false;
        }

        return collect($value)
            ->contains(fn ($item) => is_array($item) && (isset($item['class']) || isset($item['class_name']) || isset($item['confidence'])));
    }

    private function predictionToFinding(array $prediction): array
    {
        $confidence = $this->normalizeConfidence(Arr::get($prediction, 'confidence', 0));
        $rawCategory = (string) (Arr::get($prediction, 'class')
            ?? Arr::get($prediction, 'class_name')
            ?? Arr::get($prediction, 'label')
            ?? 'Detected issue');
        $category = $this->normalizeHomeInspectionCategory($rawCategory);
        $notes = 'Roboflow prediction: '.Str::limit(json_encode($prediction), 500, '...');
        $severity = $this->severityFromPrediction($category, $confidence, $notes);

        return [
            'category' => $category,
            'severity' => $severity,
            'confidence' => $confidence,
            'estimated_repair_cost' => $this->costForFinding($category, $severity, $notes),
            'notes' => $notes,
        ];
    }

    private function normalizeConfidence(mixed $value): float
    {
        if (is_array($value)) {
            $value = $value['score'] ?? $value['value'] ?? $value['confidence'] ?? $value['max'] ?? $value['min'] ?? 75;
        }

        $confidence = (float) $value;

        if ($confidence > 0 && $confidence <= 1) {
            $confidence *= 100;
        }

        return round(max(0, min(100, $confidence)), 2);
    }

    private function severityFromConfidence(float $confidence): string
    {
        return match (true) {
            $confidence >= 85 => 'high',
            $confidence >= 60 => 'medium',
            default => 'low',
        };
    }

    private function severityFromPrediction(string $category, float $confidence, string $notes): string
    {
        $text = Str::lower($category.' '.$notes);

        if (str_contains($text, 'structural') || str_contains($text, 'electrical') || str_contains($text, 'foundation')) {
            return $confidence >= 75 ? 'high' : 'medium';
        }

        if ($this->looksMinorScope($text)) {
            return $confidence >= 60 ? 'medium' : 'low';
        }

        return $this->severityFromConfidence($confidence);
    }

    private function costForFinding(string $category, string $severity, string $notes = ''): int
    {
        $text = Str::lower($category.' '.$notes);
        $minor = $this->looksMinorScope($text);

        if (str_contains(Str::lower($category), 'moisture') || str_contains($text, 'mold') || str_contains($text, 'jamur')) {
            if ($minor) {
                return $severity === 'high' ? 1200000 : ($severity === 'medium' ? 750000 : 400000);
            }

            return $severity === 'high' ? 2500000 : ($severity === 'medium' ? 1500000 : 650000);
        }

        return match (true) {
            $category === 'Ceiling' => $severity === 'high' ? 3000000 : ($severity === 'medium' ? 1500000 : 600000),
            $category === 'Walls' => $severity === 'high' ? 2500000 : ($severity === 'medium' ? 900000 : 350000),
            $category === 'Plumbing' => $severity === 'high' ? 3500000 : ($severity === 'medium' ? 1800000 : 750000),
            $category === 'Electrical' => $severity === 'high' ? 4000000 : ($severity === 'medium' ? 2000000 : 900000),
            $category === 'Roof' => $severity === 'high' ? 5000000 : ($severity === 'medium' ? 2500000 : 1000000),
            $category === 'Foundation' => $severity === 'high' ? 8000000 : ($severity === 'medium' ? 4000000 : 1500000),
            default => $severity === 'high' ? 2500000 : ($severity === 'medium' ? 1000000 : 400000),
        };
    }

    private function estimateRepairCost(array $findings): int
    {
        return min(self::MAX_STORABLE_COST, (int) collect($findings)->sum('estimated_repair_cost'));
    }

    private function extractGroqFindings(array $analyses, array $settings): array
    {
        return collect($analyses)
            ->flatMap(fn (array $analysis) => $analysis['content']['findings'] ?? [])
            ->filter(fn ($finding) => is_array($finding))
            ->map(function (array $finding) {
                $category = $this->normalizeHomeInspectionCategory($this->stringFromMixed($finding['category'] ?? 'Detected issue'));
                $severity = $this->normalizeSeverity($this->stringFromMixed($finding['severity'] ?? 'medium'));
                $confidence = $this->normalizeConfidence($finding['confidence'] ?? 75);
                $notes = Str::limit(trim($this->stringFromMixed($finding['evidence'] ?? '').' '.$this->stringFromMixed($finding['suggested_fix'] ?? '').' '.$this->stringFromMixed($finding['assumptions'] ?? '')), 500, '...');
                $estimatedCost = $this->normalizeFindingCost($finding, $category, $severity, $notes);

                return [
                    'category' => $category,
                    'severity' => $severity,
                    'confidence' => $confidence,
                    'estimated_repair_cost' => $estimatedCost,
                    'notes' => $notes,
                ];
            })
            ->unique(fn (array $finding) => Str::lower($finding['category'].'|'.$finding['severity'].'|'.$finding['notes']))
            ->values()
            ->pipe(fn ($findings) => $this->mergeSimilarFindings($findings->all()));
    }

    private function mergeSimilarFindings(array $findings): array
    {
        return collect($findings)
            ->groupBy(fn (array $finding) => $finding['category'])
            ->map(function ($group) {
                $items = collect($group)->values();

                if ($items->count() === 1) {
                    return $items->first();
                }

                $severityRank = ['low' => 1, 'medium' => 2, 'high' => 3];
                $highestSeverity = $items
                    ->pluck('severity')
                    ->sortByDesc(fn ($severity) => $severityRank[$severity] ?? 2)
                    ->first() ?? 'medium';
                $notes = $items
                    ->pluck('notes')
                    ->filter()
                    ->unique()
                    ->take(3)
                    ->implode(' ');
                $maxCost = (int) $items->max('estimated_repair_cost');
                $sumCost = (int) $items->sum('estimated_repair_cost');
                $cost = $this->looksMinorScope(Str::lower($items->first()['category'].' '.$notes))
                    ? $maxCost
                    : min($sumCost, (int) round($maxCost * 1.4));

                return [
                    'category' => $items->first()['category'],
                    'severity' => $highestSeverity,
                    'confidence' => round((float) $items->avg('confidence'), 2),
                    'estimated_repair_cost' => $this->clampFindingCost($cost, $items->first()['category'], $highestSeverity, $notes),
                    'notes' => Str::limit($notes, 500, '...'),
                ];
            })
            ->values()
            ->all();
    }

    private function normalizeHomeInspectionCategory(string $category): string
    {
        $category = Str::of($category)->lower()->replace(['_', '-'], ' ')->toString();

        return match (true) {
            str_contains($category, 'roof') => 'Roof',
            str_contains($category, 'ceiling') => 'Ceiling',
            str_contains($category, 'wall'), str_contains($category, 'crack') => 'Walls',
            str_contains($category, 'floor') => 'Floors',
            str_contains($category, 'foundation'), str_contains($category, 'structural') => 'Foundation',
            str_contains($category, 'plumb'), str_contains($category, 'pipe'), str_contains($category, 'leak') => 'Plumbing',
            str_contains($category, 'moist'), str_contains($category, 'water'), str_contains($category, 'mold') => 'Moisture and Mold Risk',
            str_contains($category, 'electric'), str_contains($category, 'wire'), str_contains($category, 'panel') => 'Electrical',
            str_contains($category, 'hvac'), str_contains($category, 'air') => 'HVAC',
            str_contains($category, 'window') => 'Windows',
            str_contains($category, 'door') => 'Doors',
            str_contains($category, 'drain'), str_contains($category, 'gutter'), str_contains($category, 'exterior') => 'Exterior Drainage',
            str_contains($category, 'pest'), str_contains($category, 'termite') => 'Pest Indicators',
            default => 'General Safety',
        };
    }

    private function normalizeSeverity(string $severity): string
    {
        $severity = Str::lower($severity);

        return match (true) {
            str_contains($severity, 'critical'), str_contains($severity, 'high') => 'high',
            str_contains($severity, 'low') => 'low',
            default => 'medium',
        };
    }

    private function normalizeCost(mixed $cost): int
    {
        if (is_array($cost)) {
            $cost = $cost['amount']
                ?? $cost['total']
                ?? $cost['value']
                ?? $cost['estimated']
                ?? $cost['max']
                ?? $cost['min']
                ?? 0;
        }

        $raw = $this->stringFromMixed($cost);
        preg_match_all('/\d+(?:[.,]\d{3})*(?:[.,]\d+)?|\d+/', $raw, $matches);

        $amounts = collect($matches[0] ?? [])
            ->map(function (string $amount) {
                $digits = preg_replace('/[^\d]/', '', $amount);

                return $digits === '' ? 0 : (int) $digits;
            })
            ->filter(fn (int $amount) => $amount > 0);

        $value = $amounts->isNotEmpty()
            ? (int) $amounts->max()
            : (int) preg_replace('/[^\d]/', '', $raw);

        return min(self::MAX_STORABLE_COST, max(0, $value));
    }

    private function normalizeFindingCost(array $finding, string $category, string $severity, string $notes): int
    {
        return $this->stableFindingCost($category, $severity, $notes);
    }

    private function stableFindingCost(string $category, string $severity, string $notes): int
    {
        $text = Str::lower($category.' '.$notes);
        $minor = $this->looksMinorScope($text);
        $categoryLower = Str::lower($category);

        $cost = match (true) {
            str_contains($categoryLower, 'moisture'), str_contains($text, 'mold'), str_contains($text, 'jamur') => $minor
                ? ($severity === 'high' ? 900000 : ($severity === 'medium' ? 650000 : 350000))
                : ($severity === 'high' ? 2200000 : ($severity === 'medium' ? 1400000 : 700000)),
            $category === 'Ceiling' => $minor
                ? ($severity === 'high' ? 1300000 : ($severity === 'medium' ? 850000 : 500000))
                : ($severity === 'high' ? 3000000 : ($severity === 'medium' ? 1800000 : 900000)),
            $category === 'Walls' => $minor
                ? ($severity === 'high' ? 900000 : ($severity === 'medium' ? 600000 : 300000))
                : ($severity === 'high' ? 2200000 : ($severity === 'medium' ? 1300000 : 600000)),
            $category === 'Plumbing' => $severity === 'high' ? 3500000 : ($severity === 'medium' ? 1800000 : 750000),
            $category === 'Electrical' => $severity === 'high' ? 4000000 : ($severity === 'medium' ? 2200000 : 900000),
            $category === 'Roof' => $severity === 'high' ? 5000000 : ($severity === 'medium' ? 2800000 : 1200000),
            $category === 'Foundation' => $severity === 'high' ? 9000000 : ($severity === 'medium' ? 5000000 : 1800000),
            default => $severity === 'high' ? 2200000 : ($severity === 'medium' ? 1000000 : 400000),
        };

        return $this->roundCost($cost);
    }

    private function roundCost(int $cost): int
    {
        return (int) max(50_000, round($cost / 50_000) * 50_000);
    }

    private function clampFindingCost(int $cost, string $category, string $severity, string $notes): int
    {
        $text = Str::lower($category.' '.$notes);
        $minor = $this->looksMinorScope($text);
        $categoryLower = Str::lower($category);

        $cap = match (true) {
            str_contains($categoryLower, 'moisture'), str_contains($text, 'mold'), str_contains($text, 'jamur') => $minor
                ? ($severity === 'high' ? 1500000 : 1000000)
                : ($severity === 'high' ? 3000000 : 2000000),
            $category === 'Ceiling' => $minor ? 1800000 : ($severity === 'high' ? 3500000 : 2500000),
            $category === 'Walls' => $minor ? 1200000 : ($severity === 'high' ? 3000000 : 2000000),
            $category === 'Plumbing' => $severity === 'high' ? 4500000 : 2500000,
            $category === 'Electrical' => $severity === 'high' ? 5000000 : 3000000,
            $category === 'Roof' => $severity === 'high' ? 6500000 : 3500000,
            $category === 'Foundation' => $severity === 'high' ? 12000000 : 7000000,
            default => $severity === 'high' ? 3500000 : 2000000,
        };

        $floor = match (true) {
            str_contains($categoryLower, 'moisture'), str_contains($text, 'mold'), str_contains($text, 'jamur') => $minor ? 250000 : 600000,
            $category === 'Ceiling' => 500000,
            $category === 'Walls' => 300000,
            default => 250000,
        };

        return max($floor, min($cost, $cap));
    }

    private function looksMinorScope(string $text): bool
    {
        if (preg_match('/(\d+(?:[.,]\d+)?)\s*(?:m2|m²|sqm|meter\s*persegi)/i', $text, $match) === 1) {
            $area = (float) str_replace(',', '.', $match[1]);

            if ($area > 0 && $area <= 2.5) {
                return true;
            }
        }

        return str_contains($text, 'small')
            || str_contains($text, 'kecil')
            || str_contains($text, 'minor')
            || str_contains($text, 'limited')
            || str_contains($text, 'surface')
            || str_contains($text, 'no structural')
            || str_contains($text, 'tidak struktural')
            || str_contains($text, '1.5m')
            || str_contains($text, '1.5 m')
            || str_contains($text, '1 m')
            || str_contains($text, 'cleaning')
            || str_contains($text, 'painting')
            || str_contains($text, 'bersihkan')
            || str_contains($text, 'cat ulang');
    }

    private function normalizeCurrency(mixed $currency): string
    {
        $currency = strtoupper((string) $currency);

        return in_array($currency, ['IDR', 'USD'], true) ? $currency : 'IDR';
    }

    private function formatMoney(int|float $amount, string $currency): string
    {
        if ($currency === 'USD') {
            return '$'.number_format((float) $amount, 2, '.', ',');
        }

        return 'Rp '.number_format((float) $amount, 0, ',', '.');
    }

    private function normalizeAnalysisOutputs(array $analyses, string $currency): array
    {
        return collect($analyses)
            ->map(function (array $analysis) use ($currency) {
                if (isset($analysis['content']) && is_array($analysis['content'])) {
                    $analysis['content'] = $this->normalizeAnalysisContent($analysis['content'], $currency);
                }

                return $analysis;
            })
            ->values()
            ->all();
    }

    private function normalizeAnalysisContent(array $content, string $currency): array
    {
        foreach (['findings', 'shopping_recommendations', 'recommended_products'] as $key) {
            if (! isset($content[$key]) || ! is_array($content[$key])) {
                continue;
            }

            $content[$key] = collect($content[$key])
                ->map(fn ($item) => is_array($item) ? $this->normalizeRecommendationOrFinding($item, $currency) : $item)
                ->values()
                ->all();
        }

        foreach (['repair_estimate', 'labor_plan', 'total_estimate'] as $key) {
            if (isset($content[$key]) && is_array($content[$key])) {
                $content[$key] = $this->normalizeMoneyStrings($content[$key], $currency);
            }
        }

        $content = $this->calibrateIndonesianLaborCosts($content, $currency);

        return $this->normalizeMoneyStrings($content, $currency);
    }

    private function calibrateIndonesianLaborCosts(array $content, string $currency): array
    {
        if ($currency !== 'IDR' || ! isset($content['labor_plan']) || ! is_array($content['labor_plan'])) {
            return $content;
        }

        $plan = $content['labor_plan'];
        $items = collect($plan['line_items'] ?? [])
            ->filter(fn ($item) => is_array($item))
            ->map(function (array $item) use ($currency) {
                $role = $this->stringFromMixed($item['role'] ?? 'Tukang');
                $rateType = Str::lower($this->stringFromMixed($item['rate_type'] ?? $item['pricing_mode'] ?? 'harian'));
                $workers = max(1, (int) ($item['worker_count'] ?? $item['workers'] ?? 1));
                $duration = $this->laborDurationDays($item['duration'] ?? 1);
                $rate = $this->normalizeCost($item['rate'] ?? 0);
                [$minimum, $maximum, $fallback] = $this->indonesianDailyLaborRange($role);

                if ($rate <= 0) {
                    $rate = $fallback;
                }

                if (str_contains($rateType, 'daily') || str_contains($rateType, 'hari') || str_contains($rateType, 'harian')) {
                    $rate = max($minimum, min($maximum, $rate));
                    $item['rate_type'] = str_contains($rateType, 'daily') ? 'daily' : 'harian';
                    $item['rate'] = $this->formatMoney($rate, $currency).'/hari';
                    $item['subtotal'] = $this->formatMoney($rate * $workers * $duration, $currency);
                }

                return $item;
            })
            ->values()
            ->all();

        if ($items === []) {
            return $content;
        }

        $laborTotal = collect($items)
            ->sum(fn (array $item) => $this->normalizeCost($item['subtotal'] ?? 0));

        $plan['line_items'] = $items;
        $plan['total_labor_cost'] = $this->formatMoney($laborTotal, $currency);
        $plan['assumptions'] = $this->appendUniqueText(
            $plan['assumptions'] ?? [],
            'Upah tukang dikalibrasi memakai kisaran harian lokal Indonesia, bukan markup kontraktor luar negeri.'
        );
        $content['labor_plan'] = $plan;

        if (isset($content['total_estimate']) && is_array($content['total_estimate'])) {
            $content['total_estimate']['labor_total'] = $this->formatMoney($laborTotal, $currency);
            $content['total_estimate']['grand_total'] = $this->formatMoney($this->recalculateGrandTotal($content['total_estimate'], $laborTotal), $currency);
        }

        return $content;
    }

    private function indonesianDailyLaborRange(string $role): array
    {
        $role = Str::lower($role);

        if (str_contains($role, 'asisten') || str_contains($role, 'helper') || str_contains($role, 'kenek')) {
            return [100_000, 200_000, 150_000];
        }

        if (str_contains($role, 'mandor') || str_contains($role, 'supervisor')) {
            return [250_000, 500_000, 350_000];
        }

        if (str_contains($role, 'listrik') || str_contains($role, 'electrical') || str_contains($role, 'plumbing') || str_contains($role, 'pipa')) {
            return [250_000, 450_000, 350_000];
        }

        return [180_000, 350_000, 275_000];
    }

    private function laborDurationDays(mixed $duration): int
    {
        if (is_numeric($duration)) {
            return max(1, (int) ceil((float) $duration));
        }

        $text = Str::lower($this->stringFromMixed($duration));
        if (preg_match('/(\d+(?:[.,]\d+)?)/', $text, $matches) !== 1) {
            return 1;
        }

        $amount = (float) str_replace(',', '.', $matches[1]);
        if (str_contains($text, 'hour') || str_contains($text, 'jam')) {
            return max(1, (int) ceil($amount / 8));
        }

        return max(1, (int) ceil($amount));
    }

    private function recalculateGrandTotal(array $estimate, int $laborTotal): int
    {
        $materials = $this->normalizeCost($estimate['materials_total'] ?? 0);
        $tools = $this->normalizeCost($estimate['tools_or_equipment_total'] ?? 0);
        $service = $this->normalizeCost($estimate['service_total'] ?? 0);
        $contingency = $this->normalizeCost($estimate['contingency'] ?? 0);

        return max($laborTotal, $materials + $laborTotal + $tools + $service + $contingency);
    }

    private function appendUniqueText(mixed $value, string $text): array
    {
        $items = is_array($value) ? $value : [$value];
        $items = collect($items)
            ->map(fn ($item) => $this->stringFromMixed($item))
            ->filter()
            ->values();

        if (! $items->contains($text)) {
            $items->push($text);
        }

        return $items->all();
    }

    private function normalizeRecommendationOrFinding(array $item, string $currency): array
    {
        $item = $this->normalizeMoneyStrings($item, $currency);
        $item = $this->calibrateAnalysisFindingCosts($item, $currency);
        $itemName = $this->stringFromMixed($item['item_name'] ?? $item['item'] ?? $item['material'] ?? $item['category'] ?? 'home repair material');
        $storeName = $this->stringFromMixed($item['store_name'] ?? $item['store'] ?? $item['vendor'] ?? 'Tokopedia');
        if (Str::of($storeName)->lower()->replace([' ', '-', '_'], '')->contains('mitra10')) {
            $storeName = 'Tokopedia';
            $item['store_name'] = 'Tokopedia';
        }
        $searchUrl = $this->marketplaceSearchUrl($storeName, $itemName);

        if ($searchUrl !== null) {
            $item['store_url'] = $searchUrl;
            $item['search_url'] = $searchUrl;
            if (empty($item['direct_product_url']) || ! $this->isProbablyValidUrl($item['direct_product_url'])) {
                $item['direct_product_url'] = $searchUrl;
            }
        }

        return $item;
    }

    private function calibrateAnalysisFindingCosts(array $item, string $currency): array
    {
        if (! array_key_exists('category', $item) && ! array_key_exists('estimated_cost', $item)) {
            return $item;
        }

        $category = $this->normalizeHomeInspectionCategory($this->stringFromMixed($item['category'] ?? 'Detected issue'));
        $severity = $this->normalizeSeverity($this->stringFromMixed($item['severity'] ?? 'medium'));
        $notes = trim($this->stringFromMixed($item['evidence'] ?? '').' '.$this->stringFromMixed($item['suggested_fix'] ?? '').' '.$this->stringFromMixed($item['assumptions'] ?? ''));
        $material = $this->normalizeCost($item['material_cost'] ?? 0);
        $labor = $this->normalizeCost($item['labor_cost'] ?? 0);
        $calibrated = $this->stableFindingCost($category, $severity, $notes);
        $item['estimated_cost'] = $this->formatMoney($calibrated, $currency);

        if ($material + $labor > 0) {
            $materialRatio = $material > 0 ? $material / max(1, $material + $labor) : 0.4;
            $calibratedMaterial = (int) round($calibrated * $materialRatio / 50000) * 50000;
            $calibratedMaterial = max(50000, min($calibrated, $calibratedMaterial));
            $calibratedLabor = max(0, $calibrated - $calibratedMaterial);
            $item['material_cost'] = $this->formatMoney($calibratedMaterial, $currency);
            $item['labor_cost'] = $this->formatMoney($calibratedLabor, $currency);
        }

        return $item;
    }

    private function normalizeMoneyStrings(mixed $value, string $currency): mixed
    {
        if (is_array($value)) {
            return collect($value)
                ->map(fn ($item) => $this->normalizeMoneyStrings($item, $currency))
                ->all();
        }

        if (! is_string($value)) {
            return $value;
        }

        $trimmed = trim($value);
        if ($trimmed === '') {
            return $value;
        }

        $hasCurrency = preg_match('/\b(rp\.?|idr|usd|\$)\b/i', $trimmed) || str_contains($trimmed, '$');
        if (! $hasCurrency) {
            return $value;
        }

        return preg_replace_callback('/(?:Rp\.?|IDR|\$|USD)?\s*\d+(?:[.,]\d{3})*(?:[.,]\d+)?/i', function (array $match) use ($currency) {
            $amount = $this->normalizeCost($match[0]);

            return $amount > 0 ? $this->formatMoney($amount, $currency) : $match[0];
        }, $trimmed) ?? $value;
    }

    private function marketplaceSearchUrl(string $storeName, string $itemName): ?string
    {
        $store = Str::of($storeName)->lower()->replace([' ', '-', '_'], '')->toString();
        $query = rawurlencode(Str::lower($itemName));

        return match (true) {
            str_contains($store, 'shopee') => "https://shopee.co.id/search?keyword={$query}",
            str_contains($store, 'ace') => "https://www.acehardware.co.id/search?query={$query}",
            str_contains($store, 'depo') => "https://depobangunan.co.id/search?keyword={$query}",
            str_contains($store, 'tukangcom'), str_contains($store, 'tukang') => 'https://tukang.com/',
            str_contains($store, 'kanggo') => 'https://www.kanggo.id/tukang-harian',
            str_contains($store, 'sejasa') => 'https://www.sejasa.com/',
            str_contains($store, 'hwb') => 'https://hwb.co.id/',
            str_contains($store, 'contractor'), str_contains($store, 'kontraktor'), str_contains($store, 'service') => 'https://www.sejasa.com/',
            default => "https://www.tokopedia.com/search?st=product&q={$query}",
        };
    }

    private function isProbablyValidUrl(mixed $url): bool
    {
        return is_string($url)
            && filter_var($url, FILTER_VALIDATE_URL)
            && Str::startsWith($url, ['https://']);
    }

    private function stringFromMixed(mixed $value): string
    {
        if ($value === null) {
            return '';
        }

        if (is_bool($value)) {
            return $value ? 'true' : 'false';
        }

        if (is_scalar($value)) {
            return trim((string) $value);
        }

        if (! is_array($value)) {
            return '';
        }

        if (array_is_list($value)) {
            return collect($value)
                ->map(fn ($item) => $this->stringFromMixed($item))
                ->filter()
                ->implode('; ');
        }

        return collect($value)
            ->map(fn ($item, string|int $key) => trim((string) $key).': '.$this->stringFromMixed($item))
            ->filter(fn (string $item) => ! str_ends_with($item, ': '))
            ->implode('; ');
    }

    private function buildAdvancedSummary(Inspection $inspection, array $analyses): string
    {
        $summaries = collect($analyses)
            ->map(fn (array $analysis) => $this->stringFromMixed($analysis['content']['summary'] ?? ''))
            ->filter()
            ->unique(fn (string $summary) => Str::lower($summary))
            ->values();

        if ($summaries->isEmpty()) {
            return 'Advanced AI report generated from Roboflow first-stage detection and stored inspection images.';
        }

        return Str::limit($summaries->implode(' '), 900, '...');
    }

    private function buildSummary(Inspection $inspection, array $results, array $findings): string
    {
        $keys = collect($results)
            ->flatMap(fn (array $result) => $result['output_keys'] ?? [])
            ->unique()
            ->implode(', ');

        if (($inspection->analysis_settings['output_language'] ?? app()->getLocale()) === 'id') {
            return sprintf(
                'Pemindaian Roboflow selesai untuk %s. %d temuan diekstrak dari output workflow. Kunci output: %s.',
                $inspection->property_name,
                count($findings),
                $keys !== '' ? $keys : 'tidak ada'
            );
        }

        return sprintf(
            'Roboflow scan completed for %s. %d findings were extracted from workflow outputs. Output keys: %s.',
            $inspection->property_name,
            count($findings),
            $keys !== '' ? $keys : 'none'
        );
    }
}
