<?php

namespace Modules\ProductCompat\Entities\Services;

use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Modules\ProductCompat\Entities\PcPartNumber;
use Modules\ProductCompat\Entities\PcDeviceModel;
use Modules\ProductCompat\Entities\PcDeviceCompat;
use Modules\ProductCompat\Entities\PcCompatible;
use Modules\ProductCompat\Entities\PcProductBarcode;
use Modules\ProductCompat\Entities\PcChangeReport;
use Modules\ProductCompat\Entities\PcProductStatus;

/**
 * Writes parsed/edited compatibility data into the normalized tables and
 * records a detailed change report. Never overwrites values that an operator
 * has already verified (verified_at set) — re-running the parser is safe.
 */
class ProductCompatService
{
    /**
     * Persist a CustomFieldParser result for one product.
     *
     * @return PcChangeReport
     */
    public function applyParseResult(int $businessId, int $productId, array $result, string $runId): PcChangeReport
    {
        $writtenParts = [];
        $writtenDevices = [];

        foreach ($result['part_numbers'] as $pn) {
            $normalized = PcPartNumber::normalize($pn['value']);
            if ($normalized === '') {
                continue;
            }

            $existing = PcPartNumber::where('business_id', $businessId)
                ->where('product_id', $productId)
                ->where('normalized', $normalized)
                ->first();

            // Respect verified manual data — don't downgrade it.
            if ($existing && ! empty($existing->verified_at)) {
                continue;
            }

            PcPartNumber::updateOrCreate(
                ['business_id' => $businessId, 'product_id' => $productId, 'normalized' => $normalized],
                [
                    'part_number'  => $pn['value'],
                    'type'         => $pn['type'] ?? 'other',
                    'source'       => 'parsed',
                    'confidence'   => $pn['confidence'],
                    'needs_review' => $pn['needs_review'],
                ]
            );
            $writtenParts[] = ['value' => $pn['value'], 'type' => $pn['type'] ?? 'other', 'confidence' => $pn['confidence']];
        }

        foreach ($result['device_models'] as $dm) {
            $model = $this->findOrCreateDeviceModel($businessId, $dm['brand'] ?? null, $dm['series'] ?? null, $dm['model']);

            $existing = PcDeviceCompat::where('product_id', $productId)
                ->where('device_model_id', $model->id)
                ->first();

            if ($existing && ! empty($existing->verified_at)) {
                continue;
            }

            PcDeviceCompat::updateOrCreate(
                ['product_id' => $productId, 'device_model_id' => $model->id],
                [
                    'business_id'  => $businessId,
                    'source'       => 'parsed',
                    'confidence'   => $dm['confidence'],
                    'needs_review' => $dm['needs_review'],
                ]
            );
            $writtenDevices[] = ['label' => $dm['label'], 'confidence' => $dm['confidence']];
        }

        $this->ensureStatus($businessId, $productId);

        return PcChangeReport::create([
            'business_id'  => $businessId,
            'product_id'   => $productId,
            'run_id'       => $runId,
            'action'       => 'parse',
            'old_data'     => $result['fields'],
            'new_data'     => [
                'part_numbers'  => $writtenParts,
                'device_models' => $writtenDevices,
                'barcodes'      => array_map(fn ($b) => $b['value'], $result['barcodes']),
            ],
            'removed_data' => array_map(fn ($r) => ['value' => $r['value'], 'reason' => $r['reason']], $result['removed']),
            'missing_data' => array_keys(array_filter($result['missing'])),
        ]);
    }

    public function findOrCreateDeviceModel(int $businessId, ?string $brand, ?string $series, string $model): PcDeviceModel
    {
        $key = PcDeviceModel::makeKey($brand, $series, $model);

        return PcDeviceModel::firstOrCreate(
            ['business_id' => $businessId, 'normalized_key' => $key],
            ['brand' => $brand, 'series' => $series, 'model' => $model]
        );
    }

    public function ensureStatus(int $businessId, int $productId): PcProductStatus
    {
        return PcProductStatus::firstOrCreate(
            ['business_id' => $businessId, 'product_id' => $productId]
        );
    }

    /**
     * Apply enrichment candidates from a provider. Only ADDS new values (never
     * touches anything already on the product, verified or not) and always
     * flags them needs_review for human approval.
     *
     * @return array{parts:int, devices:int}
     */
    public function applyEnrichmentCandidates(int $businessId, int $productId, array $candidates, string $source): array
    {
        $addedParts = 0;
        $addedDevices = 0;
        $addedBarcodes = 0;

        foreach ($candidates['part_numbers'] ?? [] as $pn) {
            $value = trim((string) ($pn['value'] ?? ''));
            if ($value === '') {
                continue;
            }
            $normalized = PcPartNumber::normalize($value);
            if ($normalized === '') {
                continue;
            }
            $exists = PcPartNumber::where('product_id', $productId)->where('normalized', $normalized)->exists();
            if ($exists) {
                continue;
            }
            PcPartNumber::create([
                'business_id'  => $businessId,
                'product_id'   => $productId,
                'part_number'  => mb_substr($value, 0, 191),
                'normalized'   => mb_substr($normalized, 0, 191),
                'type'         => in_array($pn['type'] ?? 'other', PcPartNumber::TYPES, true) ? $pn['type'] : 'other',
                'is_primary'   => false,
                'source'       => $source,
                'confidence'   => (int) ($pn['confidence'] ?? 70),
                'needs_review' => true,
            ]);
            $addedParts++;
        }

        foreach ($candidates['device_models'] ?? [] as $dm) {
            $model = trim((string) ($dm['model'] ?? ($dm['label'] ?? '')));
            $brand = trim((string) ($dm['brand'] ?? '')) ?: null;
            $series = trim((string) ($dm['series'] ?? '')) ?: null;
            if ($model === '' && $brand === null) {
                continue;
            }
            if ($model === '') {
                $model = $series ?? $brand;
            }
            if (mb_strlen($model) > 80) {
                continue; // skip blob-like models
            }
            $deviceModel = $this->findOrCreateDeviceModel($businessId, $brand, $series, $model);

            $exists = PcDeviceCompat::where('product_id', $productId)->where('device_model_id', $deviceModel->id)->exists();
            if ($exists) {
                continue;
            }
            PcDeviceCompat::create([
                'business_id'  => $businessId,
                'product_id'   => $productId,
                'device_model_id' => $deviceModel->id,
                'source'       => $source,
                'confidence'   => (int) ($dm['confidence'] ?? 70),
                'needs_review' => true,
            ]);
            $addedDevices++;
        }

        foreach ($candidates['barcodes'] ?? [] as $bc) {
            $value = preg_replace('/\D/', '', (string) ($bc['value'] ?? ''));
            if (! in_array(strlen($value), [8, 12, 13, 14], true)) {
                continue; // only valid GTIN lengths
            }
            $exists = PcProductBarcode::where('business_id', $businessId)
                ->where('product_id', $productId)->where('barcode', $value)->exists();
            if ($exists) {
                continue;
            }
            PcProductBarcode::create([
                'business_id'  => $businessId,
                'product_id'   => $productId,
                'barcode'      => $value,
                'type'         => $bc['type'] ?? 'gtin',
                'source'       => $source,
                'confidence'   => (int) ($bc['confidence'] ?? 80),
                'needs_review' => true,
            ]);
            $addedBarcodes++;
        }

        if ($addedParts || $addedDevices || $addedBarcodes) {
            $this->ensureStatus($businessId, $productId);
        }

        return ['parts' => $addedParts, 'devices' => $addedDevices, 'barcodes' => $addedBarcodes];
    }

    /**
     * Persist a manually-curated set (editor is the source of truth: rows not
     * present are removed; present rows are upserted as verified manual data).
     * Shared by the standalone editor and the product add/edit form hook.
     *
     * @param  array  $partNumbers  rows: ['part_number'=>, 'type'=>, 'is_primary'=>]
     * @param  array  $deviceModels rows: ['brand'=>, 'series'=>, 'model'=>]
     * @param  array  $compatibleProductIds  list of product ids
     */
    public function saveManualData(int $businessId, int $productId, array $partNumbers, array $deviceModels, array $compatibleProductIds, int $userId): void
    {
        $now = Carbon::now();

        DB::transaction(function () use ($businessId, $productId, $partNumbers, $deviceModels, $compatibleProductIds, $userId, $now) {
            // ---- Part numbers ----
            $keepNorm = [];
            foreach ($partNumbers as $row) {
                $value = trim($row['part_number'] ?? '');
                if ($value === '') {
                    continue;
                }
                $norm = PcPartNumber::normalize($value);
                if ($norm === '') {
                    continue;
                }
                $keepNorm[] = $norm;
                $type = in_array($row['type'] ?? 'other', PcPartNumber::TYPES, true) ? $row['type'] : 'other';

                PcPartNumber::updateOrCreate(
                    ['business_id' => $businessId, 'product_id' => $productId, 'normalized' => $norm],
                    [
                        'part_number'  => mb_substr($value, 0, 191),
                        'type'         => $type,
                        'is_primary'   => ! empty($row['is_primary']),
                        'source'       => 'manual',
                        'confidence'   => 100,
                        'needs_review' => false,
                        'verified_at'  => $now,
                        'verified_by'  => $userId,
                    ]
                );
            }
            PcPartNumber::where('product_id', $productId)
                ->when($keepNorm, fn ($q) => $q->whereNotIn('normalized', $keepNorm))
                ->delete();

            // ---- Device models ----
            $keepDeviceIds = [];
            foreach ($deviceModels as $row) {
                $model = trim($row['model'] ?? '');
                $brand = trim($row['brand'] ?? '') ?: null;
                $series = trim($row['series'] ?? '') ?: null;
                if ($model === '' && $brand === null && $series === null) {
                    continue;
                }
                if ($model === '') {
                    $model = $series ?? $brand;
                }
                $deviceModel = $this->findOrCreateDeviceModel($businessId, $brand, $series, $model);
                $keepDeviceIds[] = $deviceModel->id;

                PcDeviceCompat::updateOrCreate(
                    ['product_id' => $productId, 'device_model_id' => $deviceModel->id],
                    [
                        'business_id'  => $businessId,
                        'source'       => 'manual',
                        'confidence'   => 100,
                        'needs_review' => false,
                        'verified_at'  => $now,
                        'verified_by'  => $userId,
                    ]
                );
            }
            PcDeviceCompat::where('product_id', $productId)
                ->when($keepDeviceIds, fn ($q) => $q->whereNotIn('device_model_id', $keepDeviceIds))
                ->delete();

            // ---- Compatible products ----
            $keepCompat = array_filter(array_map('intval', $compatibleProductIds));
            foreach ($keepCompat as $compatId) {
                if ($compatId === $productId) {
                    continue;
                }
                PcCompatible::updateOrCreate(
                    ['product_id' => $productId, 'compatible_product_id' => $compatId, 'relation_type' => 'compatible'],
                    [
                        'business_id'  => $businessId,
                        'source'       => 'manual',
                        'confidence'   => 100,
                        'needs_review' => false,
                        'verified_at'  => $now,
                        'verified_by'  => $userId,
                    ]
                );
            }
            PcCompatible::where('product_id', $productId)
                ->when($keepCompat, fn ($q) => $q->whereNotIn('compatible_product_id', $keepCompat))
                ->delete();

            $this->ensureStatus($businessId, $productId);
        });
    }

    /**
     * Resolve a correction's target pc_* row for a product. Returns 'verified'
     * when the matched row is human-verified (correction suppressed), a
     * ['table'=>,'id'=>] locator when a deletable row is found, or [] when there
     * is no pc_* target (brand/name corrections hit the products row directly).
     * Shared by the online aggressive pass and the offline importer.
     *
     * @return string|array
     */
    public function resolveCorrectionTarget(int $productId, string $kind, string $current)
    {
        if (in_array($kind, ['part_number', 'part', 'remove_part_number', 'serial', 'remove_serial'], true)) {
            $norm = PcPartNumber::normalize($current);
            $row = PcPartNumber::where('product_id', $productId)->where('normalized', $norm)->first();
            if ($row) {
                return ! empty($row->verified_at) ? 'verified' : ['table' => 'pc_part_numbers', 'id' => $row->id];
            }
        }

        if (in_array($kind, ['barcode', 'gtin', 'remove_barcode'], true)) {
            $digits = preg_replace('/\D/', '', $current);
            $row = PcProductBarcode::where('product_id', $productId)->where('barcode', $digits)->first();
            if ($row) {
                return ! empty($row->verified_at) ? 'verified' : ['table' => 'pc_product_barcodes', 'id' => $row->id];
            }
        }

        if (in_array($kind, ['device_model', 'device', 'model', 'remove_device_model'], true)) {
            $modelRow = PcDeviceModel::whereRaw('LOWER(model) = ?', [strtolower($current)])->first();
            if ($modelRow) {
                $compat = PcDeviceCompat::where('product_id', $productId)->where('device_model_id', $modelRow->id)->first();
                if ($compat) {
                    return ! empty($compat->verified_at) ? 'verified' : ['table' => 'pc_device_compat', 'id' => $compat->id];
                }
            }
        }

        return [];
    }

    /**
     * Collapse duplicate identifiers on a product: part numbers / barcodes that
     * share the same normalized value. Keeps the "best" row (verified first, then
     * highest confidence, then lowest id) and deletes the rest. Exact duplicates
     * are safe to remove without review; the removals are logged to a change
     * report. Returns the number of rows removed.
     */
    public function dedupeIdentifiers(int $businessId, int $productId): int
    {
        $removed = 0;
        $removedLog = [];

        // Part numbers keyed by normalized value.
        $byNorm = PcPartNumber::where('product_id', $productId)->get()->groupBy('normalized');
        foreach ($byNorm as $norm => $group) {
            if ($norm === '' || $group->count() < 2) {
                continue;
            }
            $keep = $group->sortByDesc(fn ($r) => [$r->verified_at ? 1 : 0, (int) $r->confidence, -$r->id])->first();
            foreach ($group as $row) {
                if ($row->id !== $keep->id) {
                    $removedLog[] = ['type' => 'part_number', 'value' => $row->part_number, 'reason' => 'duplicate'];
                    $row->delete();
                    $removed++;
                }
            }
        }

        // Barcodes keyed by digits.
        $byCode = PcProductBarcode::where('product_id', $productId)->get()->groupBy('barcode');
        foreach ($byCode as $code => $group) {
            if ($code === '' || $group->count() < 2) {
                continue;
            }
            $keep = $group->sortByDesc(fn ($r) => [$r->verified_at ? 1 : 0, (int) $r->confidence, -$r->id])->first();
            foreach ($group as $row) {
                if ($row->id !== $keep->id) {
                    $removedLog[] = ['type' => 'barcode', 'value' => $row->barcode, 'reason' => 'duplicate'];
                    $row->delete();
                    $removed++;
                }
            }
        }

        if ($removed > 0) {
            PcChangeReport::create([
                'business_id'  => $businessId,
                'product_id'   => $productId,
                'run_id'       => 'dedupe_' . now()->format('YmdHis'),
                'action'       => 'ai_enrich',
                'removed_data' => $removedLog,
            ]);
        }

        return $removed;
    }

    /**
     * Mark a product as reviewed/checked. Also marks its parsed rows verified.
     */
    public function markChecked(int $businessId, int $productId, int $userId): void
    {
        $now = Carbon::now();

        $status = $this->ensureStatus($businessId, $productId);
        $status->update(['checked_at' => $now, 'checked_by' => $userId]);

        PcPartNumber::where('product_id', $productId)->whereNull('verified_at')
            ->update(['verified_at' => $now, 'verified_by' => $userId, 'needs_review' => false]);
        PcDeviceCompat::where('product_id', $productId)->whereNull('verified_at')
            ->update(['verified_at' => $now, 'verified_by' => $userId, 'needs_review' => false]);
    }
}
