<?php

namespace App\Services;

use App\Exceptions\GroqAnalysisException;
use App\Models\Inspection;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Throwable;

class GroqAnalysisClient
{
    public const MODELS = [
        'meta-llama/llama-4-scout-17b-16e-instruct' => [
            'label' => 'Llama 4 Scout',
            'kind' => 'vision',
            'rank' => 1,
        ],
        'openai/gpt-oss-120b' => [
            'label' => 'GPT OSS 120B',
            'kind' => 'text',
            'rank' => 2,
        ],
        'openai/gpt-oss-20b' => [
            'label' => 'GPT OSS 20B',
            'kind' => 'text',
            'rank' => 3,
        ],
        'qwen/qwen3-32b' => [
            'label' => 'Qwen3 32B',
            'kind' => 'text',
            'rank' => 4,
        ],
        'llama-3.3-70b-versatile' => [
            'label' => 'Llama 3.3 70B Versatile',
            'kind' => 'text',
            'rank' => 5,
        ],
        'llama-3.1-8b-instant' => [
            'label' => 'Llama 3.1 8B Instant',
            'kind' => 'text',
            'rank' => 6,
        ],
        'groq/compound' => [
            'label' => 'Groq Compound Research',
            'kind' => 'research',
            'rank' => 7,
        ],
        'groq/compound-mini' => [
            'label' => 'Groq Compound Mini',
            'kind' => 'research',
            'rank' => 8,
        ],
        'allam-2-7b' => [
            'label' => 'Allam 2 7B',
            'kind' => 'text',
            'rank' => 9,
        ],
        'openai/gpt-oss-safeguard-20b' => [
            'label' => 'Safety GPT OSS 20B',
            'kind' => 'text',
            'rank' => 10,
        ],
        'meta-llama/llama-prompt-guard-2-86m' => [
            'label' => 'Llama Prompt Guard 86M',
            'kind' => 'text',
            'rank' => 11,
        ],
        'meta-llama/llama-prompt-guard-2-22m' => [
            'label' => 'Llama Prompt Guard 22M',
            'kind' => 'text',
            'rank' => 12,
        ],
    ];

    public function analyze(Inspection $inspection, array $roboflowResults, array $settings): array
    {
        $apiKey = config('services.groq.key');

        if (! is_string($apiKey) || $apiKey === '') {
            throw GroqAnalysisException::missingApiKey();
        }

        $selectedModels = $this->selectedModels($settings);

        return collect($selectedModels)
            ->map(fn (string $model) => $this->runModelWithFallback($model, $inspection, $roboflowResults, $settings, $apiKey))
            ->values()
            ->all();
    }

    public function selectedModels(array $settings): array
    {
        $models = collect($settings['models'] ?? [])
            ->filter(fn ($model) => is_string($model) && isset(self::MODELS[$model]))
            ->unique()
            ->sortBy(fn ($model) => self::MODELS[$model]['rank'])
            ->values();

        if ($models->isEmpty()) {
            $models = collect(['meta-llama/llama-4-scout-17b-16e-instruct']);
        }

        if (! empty($settings['price_research']) && $models->doesntContain('groq/compound') && $models->doesntContain('groq/compound-mini')) {
            $models = $models->take(2)->push('groq/compound');
        }

        return $models->take(3)->values()->all();
    }

    private function runModelWithFallback(string $preferredModel, Inspection $inspection, array $roboflowResults, array $settings, string $apiKey): array
    {
        $models = collect([$preferredModel])
            ->merge($this->fallbackModels())
            ->unique()
            ->values();
        $limited = [];

        foreach ($models as $model) {
            try {
                $analysis = $this->runModel($model, $inspection, $roboflowResults, $settings, $apiKey);
                $analysis['requested_model'] = $preferredModel;
                $analysis['fallback_from'] = $model === $preferredModel ? null : $preferredModel;
                $analysis['limited_models'] = $limited;

                return $analysis;
            } catch (GroqAnalysisException $exception) {
                if (! $this->isRateLimitMessage($exception->getMessage())) {
                    throw $exception;
                }

                $limited[] = $model;
            }
        }

        throw GroqAnalysisException::requestFailed('All configured Groq report models are currently rate limited.');
    }

    private function fallbackModels(): array
    {
        return collect(array_keys(self::MODELS))
            ->sortBy(fn ($model) => self::MODELS[$model]['rank'])
            ->values()
            ->all();
    }

    private function runModel(string $model, Inspection $inspection, array $roboflowResults, array $settings, string $apiKey): array
    {
        $payload = [
            'model' => $model,
            'messages' => [
                [
                    'role' => 'system',
                    'content' => $this->systemPrompt($settings),
                ],
                [
                    'role' => 'user',
                    'content' => $this->userContent($model, $inspection, $roboflowResults, $settings),
                ],
            ],
            'temperature' => (float) ($settings['temperature'] ?? 0),
            'max_completion_tokens' => (int) ($settings['max_tokens'] ?? 2048),
            'top_p' => 0.2,
            'stream' => false,
            'response_format' => ['type' => 'json_object'],
        ];

        try {
            $response = Http::withToken($apiKey)
                ->acceptJson()
                ->timeout((int) config('services.groq.timeout', 60))
                ->post(rtrim((string) config('services.groq.api_url'), '/').'/chat/completions', $payload);
        } catch (ConnectionException $exception) {
            throw GroqAnalysisException::requestFailed($exception->getMessage());
        } catch (Throwable $exception) {
            throw GroqAnalysisException::requestFailed($exception->getMessage());
        }

        if ($response->failed()) {
            $body = Str::limit($response->body(), 500, '...');

            throw GroqAnalysisException::requestFailed("{$model} returned HTTP {$response->status()}: {$body}");
        }

        $content = $response->json('choices.0.message.content', '');
        $decoded = json_decode((string) $content, true);

        return [
            'model' => $model,
            'label' => self::MODELS[$model]['label'],
            'kind' => self::MODELS[$model]['kind'],
            'content' => is_array($decoded) ? $decoded : ['summary' => trim((string) $content)],
            'raw_text' => is_array($decoded) ? null : Str::limit((string) $content, 4000, '...'),
        ];
    }

    private function systemPrompt(array $settings): string
    {
        $currency = strtoupper((string) ($settings['currency'] ?? 'IDR'));
        $language = ((string) ($settings['output_language'] ?? app()->getLocale())) === 'id'
            ? 'Indonesian (Bahasa Indonesia)'
            : 'English';
        $estimateMode = match ((string) ($settings['detail_level'] ?? 'breakdown')) {
            'quick' => 'quick high-level home repair estimate',
            'scope' => 'repair scope with itemized cost and work priority',
            default => 'detailed material and labor cost breakdown',
        };
        $research = ! empty($settings['price_research'])
            ? 'Use current web/market research when the selected model supports it. Include store/source URLs for recommended products whenever available. If live source links are unavailable, use reputable marketplace search URLs and mark source_type as search_estimate.'
            : 'Do not claim live price research; use heuristic repair-cost estimates only. You may still provide reputable marketplace search URLs for the recommended product categories and mark source_type as search_estimate.';
        $researchDepth = match ((string) ($settings['research_depth'] ?? 'standard')) {
            'deep' => 'deep research: compare several stores, include service/labor options, mention assumptions and price variance',
            'basic' => 'basic research: include the most likely materials and one store/source per item',
            default => 'standard research: include 2-3 practical product/source options for major repair categories',
        };
        $sourceMode = match ((string) ($settings['source_link_mode'] ?? 'both')) {
            'direct' => 'prefer direct product/service URLs; use search URLs only when direct URLs are unavailable',
            'search' => 'prefer marketplace/search-result URLs that users can inspect and compare',
            default => 'include direct product/service URLs when available and fallback marketplace/search URLs',
        };
        $marketplaces = collect($settings['marketplaces'] ?? [])
            ->map(fn ($marketplace) => match ($marketplace) {
                'tokopedia' => 'Tokopedia',
                'shopee' => 'Shopee',
                'ace' => 'ACE Hardware Indonesia',
                'depo_bangunan' => 'Depo Bangunan',
                'tukang_com' => 'Tukang.com labor/service source (https://tukang.com/)',
                'kanggo' => 'Kanggo tukang harian labor source (https://www.kanggo.id/tukang-harian)',
                'sejasa' => 'Sejasa home service source (https://www.sejasa.com/)',
                'hwb' => 'HWB home service source (https://hwb.co.id/)',
                default => (string) $marketplace,
            })
            ->filter()
            ->implode(', ');
        $services = ! empty($settings['include_service_recommendations'])
            ? 'Also include recommended service/labor providers from the selected labor sources. For Indonesian handyman/labor sourcing, use only Tukang.com, Kanggo, Sejasa, and HWB links when those sources are selected.'
            : 'Focus on materials/tools; include service recommendations only when safety-critical.';
        $laborMode = match ((string) ($settings['labor_pricing_mode'] ?? 'auto')) {
            'daily' => 'daily-rate labor pricing: estimate worker roles, number of workers, work days, daily rate, and subtotal',
            'contract' => 'contract/borongan labor pricing: estimate fixed scope, crew size, expected duration, contract subtotal, and exclusions',
            'monthly' => 'monthly crew pricing: estimate monthly crew cost prorated to project duration, roles, and supervision needs',
            default => 'auto labor pricing: choose daily, contract/borongan, or monthly crew based on repair size and explain why',
        };
        $laborDetail = match ((string) ($settings['labor_detail_level'] ?? 'detailed')) {
            'forensic' => 'line-item labor detail: separate preparation, demolition/removal, repair, finishing, cleanup, inspection, and contingency labor',
            'standard' => 'standard labor detail: summarize role, duration, rate type, and subtotal',
            default => 'detailed labor detail: separate each worker role, quantity, duration, rate, subtotal, assumptions, and exclusions',
        };
        $focusPoints = collect($settings['report_points'] ?? ['materials', 'labor', 'urgency', 'safety'])
            ->map(fn ($point) => match ($point) {
                'materials' => 'materials and parts needed',
                'labor' => 'labor scope and estimated work time',
                'urgency' => 'repair urgency and risk if delayed',
                'safety' => 'safety concerns for occupants',
                'preventive_maintenance' => 'preventive maintenance recommendations',
                default => (string) $point,
            })
            ->implode(', ');

        return <<<PROMPT
You are a senior home inspection analyst. Focus only on residential property inspection categories: roof, ceiling, wall, floor, foundation, plumbing, moisture, mold risk, electrical safety, HVAC, windows, doors, exterior drainage, pest indicators, and general safety.
Return valid JSON only with keys: summary, home_inspection_categories, findings, repair_estimate, shopping_recommendations, labor_plan, total_estimate, urgency, recommended_next_steps, report_notes.
Write all human-readable text in {$language}.
Use {$currency} for every monetary estimate. Estimate mode: {$estimateMode}. Prioritize repair cost ranges, likely materials, labor assumptions, urgency, and what should be checked by a human inspector. {$research}
Research depth: {$researchDepth}. Source link mode: {$sourceMode}. Preferred stores/sources: {$marketplaces}. {$services}
Labor pricing requirement: {$laborMode}. Labor detail requirement: {$laborDetail}. Always include labor costs in total_estimate. Never provide a grand_total that omits labor.
Indonesian labor calibration: when currency is IDR or the property is in Indonesia, use local Indonesian daily worker pricing, not US contractor markup. Typical daily rates: helper/asisten Rp 100.000-Rp 200.000/day, general tukang Rp 180.000-Rp 350.000/day, specialist plumbing/electrical/waterproofing Rp 250.000-Rp 450.000/day, mandor/supervisor Rp 250.000-Rp 500.000/day. Small localized mold/ceiling patch jobs usually need 1 worker plus optional helper for 0.5-1 day; avoid 2-3 workers for several days unless area is large, demolition/replacement is needed, or access is difficult.
Report focus points requested by the user: {$focusPoints}.
For findings, include category, severity, evidence, confidence, estimated_cost, material_cost, labor_cost, suggested_fix, and assumptions.
Cost calibration rule: confidence is detection certainty, not damage severity. Do not make high-confidence small mold, small ceiling stain, or surface moisture automatically expensive. For small/localized mold or moisture stains with no structural damage, typical IDR totals should usually stay around Rp 250.000-Rp 1.500.000 depending on cleaning, anti-mold treatment, repainting, and minor patching. If the visible/assumed affected area is under about 2.5 m2, treat it as localized unless there is active leakage, electrical risk, structural damage, or repeated damage across rooms. Use multi-million estimates only when there is large area coverage, active leak repair, ceiling board replacement, plumbing work, electrical risk, structural damage, access difficulty, or multiple rooms. Do not duplicate the same physical issue across multiple findings; combine overlapping mold/moisture/ceiling findings and estimate once. State the scope assumption clearly.
For shopping_recommendations, include item_name, purpose, quantity, unit, store_name, store_url, direct_product_url, search_url, embed_url when available, product_image_url or image_url when a real HTTPS product image is available, estimated_unit_price, estimated_total_price, source_type, confidence, and notes. Do not invent image URLs; omit them if unavailable. Include detailed materials such as sealant, primer, anti-mold cleaner, replacement panels, screws, tape, waterproofing, PPE, disposal bags, and tools/services when relevant.
For labor_plan, include pricing_mode, line_items, total_labor_cost, duration_estimate, crew_summary, assumptions, exclusions, and whether pricing is daily, contract/borongan, monthly, or mixed. Each line item must include role, task, worker_count, duration, rate_type, rate, subtotal, and notes.
For total_estimate, include currency, materials_total, labor_total, tools_or_equipment_total, service_total, contingency, and grand_total.
PROMPT;
    }

    private function userContent(string $model, Inspection $inspection, array $roboflowResults, array $settings): array|string
    {
        $context = [
            'inspection' => [
                'property_name' => $inspection->property_name,
                'property_address' => $inspection->property_address,
            ],
            'currency' => strtoupper((string) ($settings['currency'] ?? 'IDR')),
            'output_language' => (string) ($settings['output_language'] ?? app()->getLocale()),
            'price_research_requested' => ! empty($settings['price_research']),
            'research_depth' => (string) ($settings['research_depth'] ?? 'standard'),
            'source_link_mode' => (string) ($settings['source_link_mode'] ?? 'both'),
            'labor_pricing_mode' => (string) ($settings['labor_pricing_mode'] ?? 'auto'),
            'labor_detail_level' => (string) ($settings['labor_detail_level'] ?? 'detailed'),
            'preferred_marketplaces' => $settings['marketplaces'] ?? [],
            'include_service_recommendations' => ! empty($settings['include_service_recommendations']),
            'roboflow_first_output' => $this->compactRoboflowContext($roboflowResults),
        ];

        $text = 'Roboflow is the first-stage detection output. Produce the next-stage home inspection analysis from this context and the available images: '.json_encode($context);

        if (self::MODELS[$model]['kind'] === 'research') {
            return $text.' Research current repair material/service price ranges and include source URLs in shopping_recommendations when possible.';
        }

        if (self::MODELS[$model]['kind'] !== 'vision') {
            return $text.' Use text-only Roboflow context because this fallback model does not accept image inputs.';
        }

        $content = [
            ['type' => 'text', 'text' => $text],
        ];

        foreach ($this->imageDataUrls($inspection) as $dataUrl) {
            $content[] = [
                'type' => 'image_url',
                'image_url' => ['url' => $dataUrl],
            ];
        }

        return $content;
    }

    private function compactRoboflowContext(array $results): array
    {
        return collect($results)
            ->map(fn (array $result) => [
                'file' => $result['file'] ?? null,
                'output_keys' => $result['output_keys'] ?? [],
                'outputs' => $this->withoutImagePayloads($result['outputs'] ?? []),
            ])
            ->values()
            ->all();
    }

    private function withoutImagePayloads(mixed $value): mixed
    {
        if (is_string($value)) {
            return Str::limit($value, 300, '...');
        }

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

        return collect($value)
            ->reject(fn ($item, $key) => in_array($key, ['path', 'url'], true))
            ->map(fn ($item) => $this->withoutImagePayloads($item))
            ->all();
    }

    private function imageDataUrls(Inspection $inspection): array
    {
        $images = collect($inspection->source_image_paths ?? [])
            ->merge($inspection->visualization_image_paths ?? [])
            ->take(5);

        return $images
            ->map(function (array $image) {
                $path = $image['path'] ?? null;

                if (! is_string($path) || ! Storage::disk('public')->exists($path)) {
                    return null;
                }

                $bytes = Storage::disk('public')->get($path);
                $mime = $this->mimeFromPath($path);

                return 'data:'.$mime.';base64,'.base64_encode($bytes);
            })
            ->filter()
            ->values()
            ->all();
    }

    private function mimeFromPath(string $path): string
    {
        return match (strtolower(pathinfo($path, PATHINFO_EXTENSION))) {
            'jpg', 'jpeg' => 'image/jpeg',
            'gif' => 'image/gif',
            'webp' => 'image/webp',
            default => 'image/png',
        };
    }

    private function isRateLimitMessage(string $message): bool
    {
        $message = Str::lower($message);

        return str_contains($message, '429')
            || str_contains($message, 'rate limit')
            || str_contains($message, 'rate_limit')
            || str_contains($message, 'quota')
            || str_contains($message, 'too many requests')
            || str_contains($message, 'tokens per minute')
            || str_contains($message, 'requests per day');
    }
}
