<?php

namespace Modules\ProductCompat\Http\Controllers;

use App\Product;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Modules\ProductCompat\Entities\PcJob;
use Modules\ProductCompat\Entities\PcSearchQueue;
use Modules\ProductCompat\Entities\Services\Settings;
use Modules\ProductCompat\Entities\Services\CustomFieldParser;
use Modules\ProductCompat\Entities\Services\ProductCompatService;
use Modules\ProductCompat\Entities\Services\EnrichmentManager;
use Modules\ProductCompat\Entities\Services\SearchQueueService;
use Modules\ProductCompat\Entities\Services\AggressiveEnrichmentService;

/**
 * Runs the pipeline (parse / enrich) from the UI in AJAX chunks so progress is
 * live and nothing blocks/timeouts — works with QUEUE_CONNECTION=sync. The
 * browser repeatedly calls runChunk() until the PcJob is done.
 */
class ProcessController extends Controller
{
    /** Products processed per chunk (parse is fast, enrich is HTTP-bound, ai_queue is AI-bound). */
    protected array $chunkSize = ['parse' => 100, 'enrich_has' => 15, 'enrich_missing' => 12, 'ai_queue' => 2];

    public function panel(Request $request)
    {
        if (! auth()->user()->can('productcompat.edit')) {
            abort(403, 'Unauthorized action.');
        }
        $business_id = $request->session()->get('user.business_id');

        $cov = \Modules\ProductCompat\Entities\Services\CoverageReport::gather($business_id);
        $coverage = [
            'with_pn'      => $this->base('enrich_has', $business_id)->count(),
            'enriched'     => DB::table('pc_enrichment_log')->where('status', 'success')->distinct()->count('product_id'),
            'without_pn'   => $this->base('enrich_missing', $business_id)->count(),
            'parseable'    => $cov['parseable'],
            'parsed'       => $cov['parsed'],
            'unparsed'     => $cov['unparsed'],
        ];
        $active = PcJob::whereIn('status', ['running'])->latest('id')->first();
        $recent = PcJob::latest('id')->limit(8)->get();
        $providers = app(EnrichmentManager::class)->availableProviders();
        $providerKeys = array_map(fn ($p) => $p->key(), $providers);
        $limitAt = Settings::get('icecat_limit_reached_at');
        $limitMsg = Settings::get('icecat_limit_message');

        // AI search-queue stats for this business (by status).
        $queueStats = ['queued' => 0, 'processing' => 0, 'done' => 0, 'failed' => 0];
        if (Schema::hasTable('pc_search_queue')) {
            $counts = PcSearchQueue::where('business_id', $business_id)
                ->select('status', DB::raw('COUNT(*) as c'))
                ->groupBy('status')->pluck('c', 'status')->toArray();
            foreach ($queueStats as $k => $_) {
                $queueStats[$k] = (int) ($counts[$k] ?? 0);
            }
        }

        return view('productcompat::process.panel', compact('coverage', 'active', 'recent', 'providerKeys', 'limitAt', 'limitMsg', 'queueStats'));
    }

    public function start(Request $request)
    {
        if (! auth()->user()->can('productcompat.edit')) {
            abort(403, 'Unauthorized action.');
        }
        $type = $request->input('type');
        if (! in_array($type, ['parse', 'enrich_has', 'enrich_missing', 'ai_queue'], true)) {
            return response()->json(['error' => 'bad type'], 422);
        }
        $business_id = $request->session()->get('user.business_id');

        if ($type === 'ai_queue') {
            // Hold the run if SearXNG is required but unreachable — surface it as
            // a stopped job so the user sees why, and leave entries queued.
            if ($hold = app(SearchQueueService::class)->searxngHoldReason()) {
                $job = PcJob::create([
                    'business_id' => $business_id,
                    'type'        => $type,
                    'status'      => 'stopped',
                    'total'       => 0,
                    'trigger'     => 'ui',
                    'started_at'  => now(),
                    'finished_at' => now(),
                    'message'     => 'Held — ' . $hold,
                ]);

                return response()->json(['job_id' => $job->id, 'total' => 0]);
            }

            // Reclaim any rows a crashed worker left stuck, then count what's queued.
            app(SearchQueueService::class)->releaseStuck();
            $total = PcSearchQueue::where('business_id', $business_id)->where('status', 'queued')->count();
        } else {
            $total = $this->base($type, $business_id)->count();
        }

        $job = PcJob::create([
            'business_id' => $business_id,
            'type'        => $type,
            'status'      => 'running',
            'total'       => $total,
            'trigger'     => 'ui',
            'started_at'  => now(),
        ]);

        return response()->json(['job_id' => $job->id, 'total' => $job->total]);
    }

    public function runChunk(Request $request, CustomFieldParser $parser, ProductCompatService $service, EnrichmentManager $manager)
    {
        if (! auth()->user()->can('productcompat.edit')) {
            abort(403, 'Unauthorized action.');
        }
        $job = PcJob::findOrFail($request->input('job_id'));
        if ($job->status !== 'running') {
            return response()->json($this->progressPayload($job));
        }

        $business_id = (int) $job->business_id;
        $size = $this->chunkSize[$job->type] ?? 20;
        $fields = config('productcompat.parse_fields', []);

        // AI search queue: claim-based, no product cursor. Claim a small batch,
        // run the aggressive enrichment for each, and aggregate the summary.
        if ($job->type === 'ai_queue') {
            return $this->runQueueChunk($job, $business_id, $size);
        }

        $rows = $this->base($job->type, $business_id)
            ->where('products.id', '>', $job->cursor)
            ->orderBy('products.id')->limit($size)->get();

        if ($rows->isEmpty()) {
            $job->update(['status' => 'done', 'finished_at' => now()]);
            return response()->json($this->progressPayload($job));
        }

        foreach ($rows as $row) {
            if ($job->type === 'parse') {
                $result = $parser->parseProduct($row, $fields);
                $service->applyParseResult($business_id, (int) $row->id, $result, 'uijob_' . $job->id);
                if ($result['part_numbers'] || $result['device_models']) {
                    $job->matched++;
                }
                $job->added_parts += count($result['part_numbers']);
                $job->added_devices += count($result['device_models']);
            } else {
                $r = $manager->enrichProduct($row);
                if ($r['status'] === 'success') {
                    $job->matched++;
                    $job->added_parts += $r['parts'];
                    $job->added_devices += $r['devices'];
                    $job->added_barcodes += $r['barcodes'] ?? 0;
                }
            }
            $job->cursor = (int) $row->id;
            $job->processed++;
        }

        // Stop early if a provider limit was hit during this run.
        $limitAt = Settings::get('icecat_limit_reached_at');
        if ($limitAt && $job->started_at && Carbon::parse($limitAt)->gte($job->started_at)) {
            $job->status = 'stopped';
            $job->message = 'Provider limit reached — update the account in Settings. ' . Settings::get('icecat_limit_message');
            $job->finished_at = now();
        } elseif ($rows->count() < $size) {
            $job->status = 'done';
            $job->finished_at = now();
        }

        $job->save();

        return response()->json($this->progressPayload($job));
    }

    /**
     * One claim-based chunk of the AI search queue. Claims up to $size queued
     * rows, runs the aggressive enrichment for each, and folds each entry's
     * summary into the job counters. An empty claim finishes the job; a budget
     * halt stops it with a message (entries stay queued — mirrors the
     * icecat-limit stop pattern).
     */
    protected function runQueueChunk(PcJob $job, int $businessId, int $size)
    {
        @ini_set('max_execution_time', '300');

        $queue   = app(SearchQueueService::class);
        $service = app(AggressiveEnrichmentService::class);

        $entries = $queue->claimBatch($businessId, $size);

        // Nothing left to claim — the queue is drained.
        if ($entries->isEmpty()) {
            $job->update(['status' => 'done', 'finished_at' => now()]);
            return response()->json($this->progressPayload($job));
        }

        $halted = false;
        foreach ($entries as $entry) {
            $res = $queue->processEntry($entry, $service);

            if (! empty($res['halt'])) {
                // Budget stop — the entry was left queued by processEntry. Do not
                // count it as processed; break out and stop the whole job.
                $halted = true;
                break;
            }

            $summary = $res['summary'] ?? [];
            $parts    = (int) ($summary['parts'] ?? 0);
            $devices  = (int) ($summary['devices'] ?? 0);
            $barcodes = (int) ($summary['barcodes'] ?? 0);

            if ($parts || $devices || $barcodes || (int) ($summary['candidates'] ?? 0)) {
                $job->matched++;
            }
            $job->added_parts    += $parts;
            $job->added_devices  += $devices;
            $job->added_barcodes += $barcodes;
            $job->processed++;
        }

        if ($halted) {
            $job->status = 'stopped';
            $job->message = 'AI monthly token budget reached — raise it in AI Settings.';
            $job->finished_at = now();
        } elseif ($entries->count() < $size) {
            // Claimed fewer than a full batch — queue is (nearly) drained.
            $job->status = 'done';
            $job->finished_at = now();
        }

        $job->save();

        return response()->json($this->progressPayload($job));
    }

    public function progress(Request $request)
    {
        $job = PcJob::findOrFail($request->input('job_id'));
        return response()->json($this->progressPayload($job));
    }

    public function stop(Request $request)
    {
        if (! auth()->user()->can('productcompat.edit')) {
            abort(403, 'Unauthorized action.');
        }
        $job = PcJob::findOrFail($request->input('job_id'));
        if ($job->status === 'running') {
            $job->update(['status' => 'stopped', 'finished_at' => now()]);
        }
        return response()->json($this->progressPayload($job));
    }

    protected function progressPayload(PcJob $job): array
    {
        return [
            'job_id'    => $job->id,
            'type'      => $job->type,
            'status'    => $job->status,
            'total'     => $job->total,
            'processed' => $job->processed,
            'percent'   => $job->percent(),
            'matched'   => $job->matched,
            'parts'     => $job->added_parts,
            'devices'   => $job->added_devices,
            'barcodes'  => $job->added_barcodes,
            'message'   => $job->message,
        ];
    }

    /**
     * Base eligible-product query per pipeline type.
     */
    protected function base(string $type, $businessId)
    {
        $q = Product::query()->select('products.*')->where('business_id', $businessId);

        if ($type === 'parse') {
            $fields = config('productcompat.parse_fields', []);
            $q->where(function ($w) use ($fields) {
                foreach ($fields as $f) {
                    $w->orWhereNotNull($f);
                }
            });
            return $q;
        }

        if ($type === 'enrich_has') {
            $q->whereExists(fn ($s) => $s->select(DB::raw(1))->from('pc_part_numbers')->whereColumn('pc_part_numbers.product_id', 'products.id'));
        } else { // enrich_missing
            $q->whereNotExists(fn ($s) => $s->select(DB::raw(1))->from('pc_part_numbers')->whereColumn('pc_part_numbers.product_id', 'products.id'));
        }
        // skip products already successfully enriched
        $q->whereNotExists(fn ($s) => $s->select(DB::raw(1))->from('pc_enrichment_log')
            ->whereColumn('pc_enrichment_log.product_id', 'products.id')->where('status', 'success'));

        return $q;
    }
}
