<?php

namespace Modules\ProductCompat\Http\Controllers;

use App\Brands;
use App\Product;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Modules\ProductCompat\Entities\PcChangeReport;
use Modules\ProductCompat\Entities\PcDataCandidate;
use Modules\ProductCompat\Entities\Services\Settings;

/**
 * Applies (or rejects) the review-first data candidates produced by the
 * aggressive / AI enrichment run. Nothing here auto-applies — a human hits
 * "Apply" per candidate. Every apply is wrapped in a transaction and writes a
 * PcChangeReport (action = ai_apply) capturing the before/after for audit.
 */
class CandidateController extends Controller
{
    /** The pc_* tables a `correction` candidate is allowed to delete a row from. */
    protected array $correctionTables = [
        'pc_part_numbers', 'pc_device_compat', 'pc_product_barcodes', 'pc_compatibles',
    ];

    /**
     * Apply a name / description / brand / specs / correction candidate.
     * image_url is handled by applyImage() (AJAX) — rejected here.
     */
    public function apply(Request $request)
    {
        if (! auth()->user()->can('productcompat.review')) {
            abort(403, 'Unauthorized action.');
        }

        $business_id = (int) $request->session()->get('user.business_id');

        $candidate = $this->pendingCandidate($request, $business_id);
        if (! $candidate) {
            return redirect()->back()->with('status', ['success' => 0, 'msg' => __('productcompat::lang.candidate_not_found')]);
        }

        if ($candidate->field === 'image_url') {
            return redirect()->back()->with('status', ['success' => 0, 'msg' => __('productcompat::lang.use_apply_image')]);
        }

        try {
            DB::transaction(function () use ($candidate, $business_id) {
                switch ($candidate->field) {
                    case 'description':
                        $this->applyDescription($candidate, $business_id);
                        break;
                    case 'name':
                        $this->applyName($candidate, $business_id);
                        break;
                    case 'brand':
                        $this->applyBrand($candidate, $business_id);
                        break;
                    case 'specs':
                        $this->applySpecs($candidate, $business_id);
                        break;
                    case 'correction':
                        $this->applyCorrection($candidate, $business_id);
                        break;
                    default:
                        throw new \RuntimeException('unsupported_field');
                }
            });
        } catch (\Throwable $e) {
            Log::warning('[ProductCompat] candidate apply failed', [
                'candidate_id' => $candidate->id,
                'field'        => $candidate->field,
                'error'        => $e->getMessage(),
            ]);

            return redirect()->back()->with('status', ['success' => 0, 'msg' => __('productcompat::lang.candidate_apply_failed')]);
        }

        return redirect()->back()->with('status', ['success' => 1, 'msg' => __('productcompat::lang.candidate_applied')]);
    }

    /**
     * Download the candidate image URL and attach it to the product. AJAX only —
     * returns JSON. Demo-env no-op + SSRF / MIME / size guards.
     */
    public function applyImage(Request $request)
    {
        if (! auth()->user()->can('productcompat.review')) {
            return response()->json(['success' => false, 'msg' => 'Unauthorized action.'], 403);
        }

        // Mirror core Util::uploadFile — no file writes in the shared demo.
        if (config('app.env') === 'demo') {
            return response()->json(['success' => false, 'msg' => __('productcompat::lang.disabled_in_demo')]);
        }

        $business_id = (int) $request->session()->get('user.business_id');

        $candidate = $this->pendingCandidate($request, $business_id);
        if (! $candidate || $candidate->field !== 'image_url') {
            return response()->json(['success' => false, 'msg' => __('productcompat::lang.candidate_not_found')]);
        }

        $url = trim((string) $candidate->value);

        // --- URL scheme guard: http/https only. ---
        if (! preg_match('#^https?://#i', $url)) {
            return response()->json(['success' => false, 'msg' => __('productcompat::lang.bad_image_url')]);
        }

        // --- SSRF guard: resolve host, reject private/loopback/reserved ranges. ---
        $host = parse_url($url, PHP_URL_HOST);
        if (! $host) {
            return response()->json(['success' => false, 'msg' => __('productcompat::lang.bad_image_url')]);
        }
        // gethostbyname returns the literal host unchanged when it can't resolve
        // (covers literal-IP hosts too — the filter flags below reject private ones).
        $ip = gethostbyname($host);
        if (! filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
            return response()->json(['success' => false, 'msg' => __('productcompat::lang.blocked_image_host')]);
        }

        try {
            $response = Http::timeout(20)
                ->withOptions(['allow_redirects' => ['max' => 3]])
                ->get($url);
        } catch (\Throwable $e) {
            return response()->json(['success' => false, 'msg' => __('productcompat::lang.image_fetch_failed')]);
        }

        if (! $response->ok()) {
            return response()->json(['success' => false, 'msg' => __('productcompat::lang.image_fetch_failed')]);
        }

        // --- Content-type must be a supported image type. ---
        $contentType = strtolower((string) $response->header('Content-Type'));
        $map = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp', 'image/gif' => 'gif'];
        $ext = null;
        foreach ($map as $mime => $e) {
            if (strpos($contentType, $mime) === 0) {
                $ext = $e;
                break;
            }
        }
        if ($ext === null) {
            return response()->json(['success' => false, 'msg' => __('productcompat::lang.bad_image_type')]);
        }

        // --- Size cap. ---
        $body = (string) $response->body();
        $maxKb = (int) Settings::get('ai_image_max_kb', 2048);
        if ($maxKb < 1) {
            $maxKb = 2048;
        }
        if (strlen($body) > $maxKb * 1024) {
            return response()->json(['success' => false, 'msg' => __('productcompat::lang.image_too_large', ['kb' => $maxKb])]);
        }

        $product = Product::where('id', $candidate->product_id)
            ->where('business_id', $business_id)->first();
        if (! $product) {
            return response()->json(['success' => false, 'msg' => __('productcompat::lang.candidate_not_found')]);
        }

        $hadImage = ! empty($product->image);
        $filename = time() . '_ai' . $product->id . '.' . $ext;

        try {
            DB::transaction(function () use ($product, $candidate, $business_id, $filename, $body, $url, $hadImage, $request) {
                // Store on the default disk (root = public/uploads) at img/<file>,
                // matching the core product-image convention.
                Storage::put('img/' . $filename, $body);

                $before = ['image' => $product->image];

                // Only overwrite an existing image when the user asked to replace it.
                if (! $hadImage || $request->boolean('replace')) {
                    $product->image = $filename;
                    $product->save();
                }

                $this->markApplied($candidate);
                $this->writeReport($candidate, $business_id, $before, [
                    'image'      => $product->image,
                    'source_url' => $url,
                    'stored_as'  => $filename,
                ]);
            });
        } catch (\Throwable $e) {
            Log::warning('[ProductCompat] image apply failed', [
                'candidate_id' => $candidate->id,
                'error'        => $e->getMessage(),
            ]);

            return response()->json(['success' => false, 'msg' => __('productcompat::lang.candidate_apply_failed')]);
        }

        $product->refresh();

        return response()->json([
            'success'   => true,
            'msg'       => __('productcompat::lang.image_attached'),
            'image_url' => $product->image_url,
        ]);
    }

    /**
     * Reject one or more candidates (single id or bulk ids[]), like
     * ReviewController::reject supports both shapes.
     */
    public function reject(Request $request)
    {
        if (! auth()->user()->can('productcompat.review')) {
            abort(403, 'Unauthorized action.');
        }

        $business_id = (int) $request->session()->get('user.business_id');
        $uid = (int) auth()->id();

        $ids = array_filter(array_map('intval', (array) $request->input('ids', [])));
        if (! $ids && $request->filled('id')) {
            $ids = [(int) $request->input('id')];
        }
        if (! $ids) {
            return redirect()->back()->with('status', ['success' => 0, 'msg' => __('productcompat::lang.candidate_not_found')]);
        }

        $count = PcDataCandidate::where('business_id', $business_id)
            ->whereIn('id', $ids)
            ->where('status', 'pending')
            ->update(['status' => 'rejected', 'decided_by' => $uid]);

        return redirect()->back()->with('status', ['success' => 1, 'msg' => __('productcompat::lang.candidate_rejected', ['count' => $count])]);
    }

    // --------------------------------------------------------------------- //
    //  Field appliers
    // --------------------------------------------------------------------- //

    protected function applyDescription(PcDataCandidate $candidate, int $businessId): void
    {
        $product = $this->product($candidate, $businessId);
        $before = ['product_description' => $product->product_description];

        $product->product_description = (string) $candidate->value;
        $product->save();

        $this->markApplied($candidate);
        $this->writeReport($candidate, $businessId, $before, ['product_description' => $product->product_description]);
    }

    protected function applyName(PcDataCandidate $candidate, int $businessId): void
    {
        $product = $this->product($candidate, $businessId);
        $before = ['name' => $product->name];

        $product->name = mb_substr((string) $candidate->value, 0, 191);
        $product->save();

        $this->markApplied($candidate);
        $this->writeReport($candidate, $businessId, $before, ['name' => $product->name]);
    }

    protected function applyBrand(PcDataCandidate $candidate, int $businessId): void
    {
        $product = $this->product($candidate, $businessId);
        $before = ['brand_id' => $product->brand_id];

        $name = trim((string) $candidate->value);
        $brand = Brands::firstOrCreate(
            ['business_id' => $businessId, 'name' => $name],
            ['created_by' => (int) auth()->id()]
        );

        $product->brand_id = $brand->id;
        $product->save();

        $this->markApplied($candidate);
        $this->writeReport($candidate, $businessId, $before, ['brand_id' => $product->brand_id, 'brand' => $name]);
    }

    protected function applySpecs(PcDataCandidate $candidate, int $businessId): void
    {
        $product = $this->product($candidate, $businessId);
        $before = ['product_description' => $product->product_description];

        $rows = json_decode((string) $candidate->value, true);
        $rows = is_array($rows) ? $rows : [];

        $html = '<br><strong>' . e(__('productcompat::lang.specifications')) . '</strong><ul>';
        foreach ($rows as $row) {
            $label = trim((string) ($row['label'] ?? ''));
            $val   = trim((string) ($row['value'] ?? ''));
            if ($label === '' && $val === '') {
                continue;
            }
            $html .= '<li>' . e($label) . ': ' . e($val) . '</li>';
        }
        $html .= '</ul>';

        $product->product_description = (string) $product->product_description . $html;
        $product->save();

        $this->markApplied($candidate);
        $this->writeReport($candidate, $businessId, $before, ['product_description' => $product->product_description]);
    }

    protected function applyCorrection(PcDataCandidate $candidate, int $businessId): void
    {
        $extra = (array) $candidate->extra;
        $table = (string) ($extra['target_table'] ?? '');
        $id    = (int) ($extra['target_id'] ?? 0);

        if (! in_array($table, $this->correctionTables, true) || $id < 1) {
            throw new \RuntimeException('bad_correction_target');
        }

        $row = DB::table($table)->where('id', $id)->first();
        if (! $row || (int) ($row->business_id ?? 0) !== $businessId) {
            throw new \RuntimeException('correction_target_missing');
        }
        if (! empty($row->verified_at)) {
            throw new \RuntimeException('correction_target_verified');
        }

        DB::table($table)->where('id', $id)->delete();

        $this->markApplied($candidate);
        $this->writeReport($candidate, $businessId, [
            'removed_from' => $table,
            'removed_row'  => (array) $row,
        ], [
            'kind'    => $extra['kind'] ?? null,
            'current' => $extra['current_value'] ?? null,
            'problem' => $extra['problem'] ?? null,
        ]);
    }

    // --------------------------------------------------------------------- //
    //  Helpers
    // --------------------------------------------------------------------- //

    /**
     * Load the candidate by id, validating it belongs to this business and is
     * still pending. Returns null otherwise (caller flashes an error).
     */
    protected function pendingCandidate(Request $request, int $businessId): ?PcDataCandidate
    {
        $id = (int) $request->input('candidate_id', $request->input('id'));
        if ($id < 1) {
            return null;
        }

        $candidate = PcDataCandidate::find($id);
        if (! $candidate
            || (int) $candidate->business_id !== $businessId
            || $candidate->status !== 'pending') {
            return null;
        }

        return $candidate;
    }

    /** Load + business-scope the product a candidate targets. */
    protected function product(PcDataCandidate $candidate, int $businessId): Product
    {
        $product = Product::where('id', $candidate->product_id)
            ->where('business_id', $businessId)->first();
        if (! $product) {
            throw new \RuntimeException('product_missing');
        }

        return $product;
    }

    /** Flip a candidate to applied (status + timestamp + decider). */
    protected function markApplied(PcDataCandidate $candidate): void
    {
        $candidate->status = 'applied';
        $candidate->applied_at = now();
        $candidate->decided_by = (int) auth()->id();
        $candidate->save();
    }

    /** Write the ai_apply audit row (before/after). */
    protected function writeReport(PcDataCandidate $candidate, int $businessId, array $old, array $new): void
    {
        PcChangeReport::create([
            'business_id'  => $businessId,
            'product_id'   => (int) $candidate->product_id,
            'run_id'       => mb_substr('apply_' . date('YmdHis'), 0, 40),
            'action'       => 'ai_apply',
            'old_data'     => $old,
            'new_data'     => array_merge($new, [
                'field'        => $candidate->field,
                'source'       => $candidate->source,
                'confidence'   => $candidate->confidence,
                'candidate_id' => $candidate->id,
            ]),
            'removed_data' => null,
            'missing_data' => null,
        ]);
    }
}
