<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Concerns\AuthorizesCompanyAccess;
use App\Http\Controllers\Controller;
use App\Services\DataverseService;
use App\Services\SharePointService;
use Carbon\Carbon;
use Illuminate\Support\Facades\Mail;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Throwable;

class FeedbackController extends Controller
{
    use AuthorizesCompanyAccess;

    public function __construct(private DataverseService $dv, private SharePointService $sp, private \App\Services\Identity\AppUserService $appUsers) {}

    // cr6f2_category labels
    private const CATEGORY_LABELS = [
        521880000 => 'OCR wrong field',
        521880001 => 'UI bug',
        521880002 => 'Performance',
        521880003 => 'Suggestion',
        521880004 => 'Other',
    ];
    private const CATEGORY_VALUES = [521880000, 521880001, 521880002, 521880003, 521880004];

    private const SEVERITY_LABELS = [
        521880000 => 'Low',
        521880001 => 'Medium',
        521880002 => 'High',
        521880003 => 'Blocker',
    ];
    private const SEVERITY_VALUES = [521880000, 521880001, 521880002, 521880003];

    private const STATUS_LABELS = [
        521880000 => 'Open',
        521880001 => 'Reviewing',
        521880002 => 'Resolved',
    ];
    private const STATUS_VALUES = [521880000, 521880001, 521880002];

    // Feedback report uses Malaysia local time for date bucketing — matches AI Usage report.
    private const REPORT_TZ = 'Asia/Kuala_Lumpur';

    // POST /api/v1/feedback — any authenticated user submits feedback.
    public function submit(Request $request): JsonResponse
    {
        $data = $request->validate([
            'category'    => ['required', 'integer', Rule::in(self::CATEGORY_VALUES)],
            'severity'    => ['required', 'integer', Rule::in(self::SEVERITY_VALUES)],
            'message'     => ['required', 'string', 'max:4000'],
            'page_name'   => ['sometimes', 'nullable', 'string', 'max:500'],
            'document_id' => ['sometimes', 'nullable', 'uuid'],
            'field_name'  => ['sometimes', 'nullable', 'string', 'max:100'],
            'user_agent'  => ['sometimes', 'nullable', 'string', 'max:500'],
            'company_id'        => ['sometimes', 'nullable', 'uuid'],
            'screenshots'         => ['sometimes', 'nullable', 'array', 'max:10'],
            'screenshots.*.b64'  => ['required_with:screenshots', 'string'],
            'screenshots.*.name' => ['required_with:screenshots', 'string', 'max:255'],
        ]);

        $message = trim($data['message']);
        if ($message === '') {
            return response()->json(['success' => false, 'message' => 'Message cannot be empty.'], 422);
        }

        // Server-side defensive truncation — clients might bypass HTML maxLength.
        $pageName  = Str::limit($data['page_name']  ?? '', 500, '');
        $userAgent = Str::limit($data['user_agent'] ?? '', 500, '');
        $fieldName = Str::limit($data['field_name'] ?? '', 100, '');

        // Resolve submitter's systemuser GUID. Required by Submitted By lookup.
        $email = strtolower((string) ($request->user()?->email ?? ''));
        if ($email === '') {
            return response()->json(['success' => false, 'message' => 'Unauthenticated.'], 401);
        }
        $userGuid = null;
        try {
            $safe = str_replace("'", "''", $email);
            $su = $this->dv->get('systemusers', [
                'select' => ['systemuserid'],
                'filter' => "internalemailaddress eq '{$safe}'",
                'top'    => 1,
            ]);
            $userGuid = $su['value'][0]['systemuserid'] ?? null;
        } catch (Throwable $e) {
            Log::warning('feedback submit: systemuser lookup failed', ['email' => $email, 'error' => $e->getMessage()]);
        }
        // Attribution: app-user column when enabled, else systemuser. Feedback must
        // be attributed (Submitted By), so fail only if neither resolves — this also
        // lets a non-Microsoft (Google/email) user submit once app-users are enabled.
        $submittedBind = $this->appUsers->attributionBind('SubmittedBy', $email, $request->user()?->name, $userGuid);
        if (empty($submittedBind)) {
            return response()->json([
                'success' => false,
                'message' => 'Unable to submit feedback because your user record could not be resolved.',
            ], 422);
        }

        $payload = [
            'cr6f2_category' => (int) $data['category'],
            'cr6f2_severity' => (int) $data['severity'],
            'cr6f2_message'  => $message,
            'cr6f2_pagename' => $pageName,
            'cr6f2_useragent'=> $userAgent,
            'cr6f2_fieldname'=> $fieldName,
            'cr6f2_status'   => 521880000, // Open
            ...$submittedBind,
        ];
        if (!empty($data['document_id'])) {
            $payload['cr6f2_Document@odata.bind'] = "/cr6f2_documentses({$data['document_id']})";
        }
        if (!empty($data['company_id'])) {
            $payload['cr6f2_Company@odata.bind'] = "/cr6f2_companies({$data['company_id']})";
        }

        try {
            $newId = $this->dv->create('cr6f2_userfeedbacks', $payload);
        } catch (Throwable $e) {
            // If document binding failed because the GUID belongs to a different entity
            // (e.g. a bank-statement ID passed on the recon page), retry without it.
            if (!empty($data['document_id']) && str_contains($e->getMessage(), 'Does Not Exist')) {
                Log::warning('feedback submit: document_id not found in cr6f2_documentses, retrying without bind', ['document_id' => $data['document_id']]);
                unset($payload['cr6f2_Document@odata.bind']);
                try {
                    $newId = $this->dv->create('cr6f2_userfeedbacks', $payload);
                } catch (Throwable $e2) {
                    return response()->json(['success' => false, 'message' => 'Failed to save feedback: ' . $e2->getMessage()], 502);
                }
            } else {
                return response()->json(['success' => false, 'message' => 'Failed to save feedback: ' . $e->getMessage()], 502);
            }
        }

        // Legacy mirror — if submitted from a review page (docId present),
        // also write the message to document.cr6f2_reviewcomment so the
        // existing review UI keeps showing it. Best-effort: feedback row is
        // saved either way.
        //
        // Only mirror when the caller may access the document's company — the
        // feedback form is open to any authenticated user, but overwriting
        // another tenant's review comment would be cross-tenant tampering.
        if (!empty($data['document_id'])) {
            $mayMirror = false;
            try {
                $docRec     = $this->dv->find('cr6f2_documentses', $data['document_id'], ['select' => ['_cr6f2_companyid_value']]);
                $docCompany = $docRec['_cr6f2_companyid_value'] ?? null;
                $mayMirror  = $docCompany && $this->authorizeCompany($docCompany) === null;
            } catch (Throwable) {
                $mayMirror = false; // not a document row (e.g. a bank-statement id) — skip the mirror
            }
            if ($mayMirror) {
                try {
                    $this->dv->update('cr6f2_documentses', $data['document_id'], ['cr6f2_reviewcomment' => $message]);
                } catch (Throwable $e) {
                    Log::warning('feedback submit: review_comment mirror failed', [
                        'feedback_id' => $newId,
                        'document_id' => $data['document_id'],
                        'error'       => $e->getMessage(),
                    ]);
                }
            }
        }

        // Step A — Upload screenshots to SharePoint Feedback folder (best-effort, up to 10).
        $screenshotUrls = [];
        $siteUrl = rtrim(config('services.sharepoint.site_url', ''), '/');
        foreach ($data['screenshots'] ?? [] as $i => $shot) {
            $tmp = null;
            try {
                $raw   = preg_replace('/^data:[^;]+;base64,/', '', $shot['b64']);
                $bytes = base64_decode($raw, true);
                if ($bytes === false || strlen($bytes) === 0) continue;
                $ext = strtolower(pathinfo($shot['name'] ?? 'screenshot.png', PATHINFO_EXTENSION)) ?: 'png';
                $tmp = tempnam(sys_get_temp_dir(), 'ocr_fb_') . '.' . $ext;
                file_put_contents($tmp, $bytes);
                $ts  = now()->format('Ymd_His');
                $url = $this->sp->uploadFile(
                    $siteUrl . '/Shared Documents/Server/Outsourcing/OcrPlayground',
                    'Feedback',
                    "screenshot_{$newId}_{$ts}_{$i}.{$ext}",
                    $tmp,
                );
                if ($url) $screenshotUrls[] = $url;
            } catch (Throwable $e) {
                Log::warning('feedback submit: screenshot upload failed', ['feedback_id' => $newId, 'index' => $i, 'error' => $e->getMessage()]);
            } finally {
                if ($tmp && file_exists($tmp)) @unlink($tmp);
            }
        }

        // Persist all screenshot URLs (newline-separated) to cr6f2_screenshoturl (best-effort).
        if (!empty($screenshotUrls)) {
            try {
                $this->dv->update('cr6f2_userfeedbacks', $newId, [
                    'cr6f2_screenshoturl' => implode("\n", $screenshotUrls),
                ]);
            } catch (Throwable $e) {
                Log::warning('feedback submit: screenshot url save failed', ['feedback_id' => $newId, 'error' => $e->getMessage()]);
            }
        }

        // Fetch linked document metadata for the email (best-effort).
        $docLink = null;
        $docName = null;
        if (!empty($data['document_id'])) {
            try {
                $doc     = $this->dv->find('cr6f2_documentses', $data['document_id'], [
                    'select' => ['cr6f2_documentlink', 'cr6f2_documentstoredlink', 'cr6f2_documentname'],
                ]);
                $docLink = $doc['cr6f2_documentlink'] ?? $doc['cr6f2_documentstoredlink'] ?? null;
                $docName = $doc['cr6f2_documentname'] ?? null;
            } catch (Throwable) {}
        }

        // Step B — Notify developers via system log (bell) + email.
        $this->notifyDevelopersOfFeedback(
            (int) $data['category'],
            (int) $data['severity'],
            $message,
            $request->user()?->name ?? $email,
            [
                'page_name'      => $pageName,
                'document_id'    => $data['document_id'] ?? null,
                'document_link'  => $docLink,
                'document_name'  => $docName,
                'screenshot_urls' => $screenshotUrls,
                'feedback_id'    => $newId,
            ]
        );

        return response()->json(['success' => true, 'data' => ['id' => $newId]]);
    }

    private function notifyDevelopersOfFeedback(
        int $category, int $severity, string $message, string $submitter,
        array $ctx = []
    ): void {
        try {
            // Find every distinct developer with GUID + email via PIC map expand.
            $picRows = $this->dv->get('cr6f2_companypicmaps', [
                'select' => ['_cr6f2_picuser_value'],
                'filter' => 'cr6f2_isdeveloper eq true and statecode eq 0',
                'expand' => 'cr6f2_PicUser($select=systemuserid,internalemailaddress)',
                'top'    => 50,
            ])['value'] ?? [];

            // Keep each dev's guid and email PAIRED — the notification bind below
            // needs that dev's own email, and deduping them into two independent
            // lists would let the indexes drift apart and notify the wrong person.
            $devs      = [];
            $devGuids  = [];
            $devEmails = [];
            foreach ($picRows as $r) {
                $guid  = $r['_cr6f2_picuser_value'] ?? null;
                $email = $r['cr6f2_PicUser']['internalemailaddress'] ?? null;
                if ($guid && !in_array($guid, $devGuids, true)) {
                    $devGuids[] = $guid;
                    $devs[]     = ['guid' => $guid, 'email' => $email];
                }
                if ($email && !in_array($email, $devEmails, true)) $devEmails[] = $email;
            }

            if (empty($devGuids)) return;

            $sevLabel = self::SEVERITY_LABELS[$severity] ?? 'Unknown';
            $catLabel = self::CATEGORY_LABELS[$category] ?? 'Unknown';
            $snippet  = mb_substr($message, 0, 80) . (mb_strlen($message) > 80 ? '…' : '');
            $logMsg   = "New feedback · {$sevLabel} {$catLabel} from {$submitter} — {$snippet}";
            $logType  = match ($severity) {
                521880003 => 521880002, // Error
                521880002 => 521880001, // Warning
                default   => 521880000, // Info
            };
            $ts = now()->toIso8601String();

            // System-log per dev (bell notification)
            foreach ($devs as $dev) {
                $guid = $dev['guid'];
                try {
                    $this->dv->create('cr6f2_systemlogs', [
                        'cr6f2_category'          => 521880008,
                        'cr6f2_message'           => $logMsg,
                        'cr6f2_logtype'           => $logType,
                        'cr6f2_timestamp'         => $ts,
                        // That dev's own email, not the request user's — this row
                        // is the recipient's bell notification, and the bell
                        // filters per-user, so a wrong bind hides it from them.
                        ...$this->appUsers->attributionBind('UserID', $dev['email'], null, $guid),
                    ]);
                } catch (Throwable $e) {
                    Log::warning('feedback notify: system log write failed', ['dev_guid' => $guid, 'error' => $e->getMessage()]);
                }
            }

            // Email notification (silent until MAIL_MAILER=smtp is configured in .env)
            if (!empty($devEmails)) {
                try {
                    $pageName       = $ctx['page_name']      ?? '';
                    $docId          = $ctx['document_id']    ?? '';
                    $docLink        = $ctx['document_link']  ?? '';
                    $docName        = $ctx['document_name']  ?? '';
                    $screenshotUrls = $ctx['screenshot_urls'] ?? [];
                    $feedbackId     = $ctx['feedback_id']    ?? '';

                    $subject = "[OCR Feedback] {$sevLabel} · {$catLabel} from {$submitter}";

                    $lines = [
                        "New feedback received via SYHC OCR Invoice System.",
                        "",
                        "From      : {$submitter}",
                        "Severity  : {$sevLabel}",
                        "Category  : {$catLabel}",
                        "Page      : " . ($pageName ?: '(not provided)'),
                        "",
                        "Message:",
                        $message,
                    ];
                    if ($docId) {
                        $lines[] = "";
                        $lines[] = "Document ID : {$docId}";
                        if ($docName) $lines[] = "Document    : {$docName}";
                        if ($docLink) $lines[] = "Link        : {$docLink}";
                    }
                    if (!empty($screenshotUrls)) {
                        $lines[] = "";
                        if (count($screenshotUrls) === 1) {
                            $lines[] = "Screenshot  : {$screenshotUrls[0]}";
                        } else {
                            $lines[] = "Screenshots (" . count($screenshotUrls) . "):";
                            foreach ($screenshotUrls as $idx => $url) {
                                $lines[] = "  " . ($idx + 1) . ". {$url}";
                            }
                        }
                    }
                    if ($feedbackId) {
                        $lines[] = "";
                        $lines[] = "Feedback ID : {$feedbackId}";
                    }

                    $body = implode("\n", $lines);
                    Mail::raw($body, function ($mail) use ($devEmails, $subject) {
                        $mail->to($devEmails)->subject($subject);
                    });
                } catch (Throwable $e) {
                    Log::warning('feedback notify: email send failed', ['error' => $e->getMessage()]);
                }
            }
        } catch (Throwable $e) {
            Log::warning('feedback notify: dev lookup failed', ['error' => $e->getMessage()]);
        }
    }

    // GET /api/v1/developer/feedback-screenshot?url={encoded_sharepoint_url}
    // Proxies a SharePoint screenshot through the backend so the browser doesn't
    // need a SharePoint session to display it.
    public function screenshotProxy(Request $request): \Illuminate\Http\Response
    {
        $rawUrl  = (string) $request->query('url', '');
        $siteUrl = rtrim(config('services.sharepoint.site_url', ''), '/');

        if (!$rawUrl || !str_starts_with($rawUrl, $siteUrl)) {
            return response('Invalid URL', 403);
        }

        try {
            $bytes = $this->sp->downloadFileBytes($rawUrl);
        } catch (Throwable $e) {
            return response('Failed to fetch screenshot: ' . $e->getMessage(), 502);
        }

        $ext  = strtolower(pathinfo(parse_url($rawUrl, PHP_URL_PATH) ?? '', PATHINFO_EXTENSION)) ?: 'png';
        $mime = match ($ext) {
            'jpg', 'jpeg' => 'image/jpeg',
            'gif'         => 'image/gif',
            'webp'        => 'image/webp',
            default       => 'image/png',
        };

        return response($bytes, 200, [
            'Content-Type'  => $mime,
            'Cache-Control' => 'private, max-age=300',
        ]);
    }

    // GET /api/v1/developer/feedback?company_id=&from=&to=&status=&category=&severity=&limit=
    public function listFeedback(Request $request): JsonResponse
    {
        $request->validate([
            'company_id' => ['sometimes', 'nullable', 'uuid'],
            'from'       => ['sometimes', 'nullable', 'date'],
            'to'         => ['sometimes', 'nullable', 'date'],
            'status'     => ['sometimes', 'nullable', 'integer', Rule::in(self::STATUS_VALUES)],
            'category'   => ['sometimes', 'nullable', 'integer', Rule::in(self::CATEGORY_VALUES)],
            'severity'   => ['sometimes', 'nullable', 'integer', Rule::in(self::SEVERITY_VALUES)],
            'limit'      => ['sometimes', 'integer', 'min:0', 'max:5000'],
        ]);

        $filters = ['statecode eq 0'];
        if ($cid = $request->query('company_id')) {
            $filters[] = "_cr6f2_company_value eq {$cid}";
        }
        if ($request->filled('from')) {
            $iso = Carbon::parse($request->from, self::REPORT_TZ)->startOfDay()->utc()->toIso8601String();
            $filters[] = "createdon ge {$iso}";
        }
        if ($request->filled('to')) {
            $iso = Carbon::parse($request->to, self::REPORT_TZ)->endOfDay()->utc()->toIso8601String();
            $filters[] = "createdon le {$iso}";
        }
        if ($request->filled('status'))   $filters[] = "cr6f2_status eq " . (int) $request->status;
        if ($request->filled('category')) $filters[] = "cr6f2_category eq " . (int) $request->category;
        if ($request->filled('severity')) $filters[] = "cr6f2_severity eq " . (int) $request->severity;

        $limit = (int) $request->query('limit', 100);
        if ($limit <= 0 || $limit > 5000) $limit = 5000; // 0 (All) → 5000 cap

        try {
            $result = $this->dv->get('cr6f2_userfeedbacks', [
                'select'  => [
                    'cr6f2_userfeedbackid',
                    'cr6f2_name',
                    'cr6f2_category',
                    'cr6f2_severity',
                    'cr6f2_message',
                    'cr6f2_pagename',
                    'cr6f2_fieldname',
                    'cr6f2_useragent',
                    'cr6f2_status',
                    'cr6f2_feedbackhistory',
                    'cr6f2_screenshoturl',
                    'createdon',
                    '_cr6f2_company_value',
                    '_cr6f2_document_value',
                    '_cr6f2_submittedby_value',
                ],
                // Expand the linked document so the UI can offer an "Open Document" button.
                'expand'  => 'cr6f2_Document($select=cr6f2_documentlink,cr6f2_documentstoredlink)',
                'filter'  => implode(' and ', $filters),
                'orderby' => 'createdon desc',
                'top'     => $limit,
            ]);
        } catch (Throwable $e) {
            return response()->json(['success' => false, 'message' => 'Failed to fetch feedback: ' . $e->getMessage()], 502);
        }

        $rows = array_map(fn($r) => [
            'id'             => $r['cr6f2_userfeedbackid'],
            'name'           => $r['cr6f2_name'] ?? null,
            'category'       => $r['cr6f2_category'] ?? null,
            'category_label' => self::CATEGORY_LABELS[$r['cr6f2_category'] ?? -1] ?? null,
            'severity'       => $r['cr6f2_severity'] ?? null,
            'severity_label' => self::SEVERITY_LABELS[$r['cr6f2_severity'] ?? -1] ?? null,
            'message'        => $r['cr6f2_message'] ?? null,
            'page_name'      => $r['cr6f2_pagename'] ?? null,
            'field_name'     => $r['cr6f2_fieldname'] ?? null,
            'user_agent'     => $r['cr6f2_useragent'] ?? null,
            'status'         => $r['cr6f2_status'] ?? null,
            'status_label'   => self::STATUS_LABELS[$r['cr6f2_status'] ?? -1] ?? null,
            'feedback_history' => $r['cr6f2_feedbackhistory'] ?? null,
            'screenshot_urls' => !empty($r['cr6f2_screenshoturl'])
                ? array_values(array_filter(explode("\n", $r['cr6f2_screenshoturl'])))
                : [],
            'created_on'      => $r['createdon'] ?? null,
            'company_id'     => $r['_cr6f2_company_value'] ?? null,
            'company_name'   => $r['_cr6f2_company_value@OData.Community.Display.V1.FormattedValue'] ?? null,
            'document_id'    => $r['_cr6f2_document_value'] ?? null,
            'document_name'  => $r['_cr6f2_document_value@OData.Community.Display.V1.FormattedValue'] ?? null,
            // Prefer the original SharePoint link; fall back to the stored copy. Null when no document is linked.
            'document_link'  => $r['cr6f2_Document']['cr6f2_documentlink'] ?? $r['cr6f2_Document']['cr6f2_documentstoredlink'] ?? null,
            'submitted_by_id'   => $r['_cr6f2_submittedby_value'] ?? null,
            'submitted_by_name' => $r['_cr6f2_submittedby_value@OData.Community.Display.V1.FormattedValue'] ?? null,
        ], $result['value'] ?? []);

        return response()->json(['success' => true, 'data' => $rows]);
    }

    // GET /api/v1/developer/review-comments?company_id=&from=&to=&limit=
    // Returns documents whose cr6f2_reviewcomment is non-null. Includes
    // cr6f2_maindoctype so the frontend can deep-link to the right review page.
    public function listReviewComments(Request $request): JsonResponse
    {
        $request->validate([
            'company_id' => ['sometimes', 'nullable', 'uuid'],
            'from'       => ['sometimes', 'nullable', 'date'],
            'to'         => ['sometimes', 'nullable', 'date'],
            'limit'      => ['sometimes', 'integer', 'min:0', 'max:5000'],
        ]);

        $filters = ['statecode eq 0', 'cr6f2_reviewcomment ne null'];
        if ($cid = $request->query('company_id')) {
            $filters[] = "_cr6f2_companyid_value eq {$cid}";
        }
        if ($request->filled('from')) {
            $iso = Carbon::parse($request->from, self::REPORT_TZ)->startOfDay()->utc()->toIso8601String();
            $filters[] = "modifiedon ge {$iso}";
        }
        if ($request->filled('to')) {
            $iso = Carbon::parse($request->to, self::REPORT_TZ)->endOfDay()->utc()->toIso8601String();
            $filters[] = "modifiedon le {$iso}";
        }

        $limit = (int) $request->query('limit', 100);
        if ($limit <= 0 || $limit > 5000) $limit = 5000; // 0 (All) → 5000 cap

        try {
            $result = $this->dv->get('cr6f2_documentses', [
                'select'  => [
                    'cr6f2_documentsid',
                    'cr6f2_documentname',
                    'cr6f2_maindoctype',
                    'cr6f2_reviewcomment',
                    'cr6f2_documentlink',
                    'cr6f2_documentstoredlink',
                    'modifiedon',
                    '_cr6f2_companyid_value',
                    '_modifiedby_value',
                ],
                'filter'  => implode(' and ', $filters),
                'orderby' => 'modifiedon desc',
                'top'     => $limit,
            ]);
        } catch (Throwable $e) {
            return response()->json(['success' => false, 'message' => 'Failed to fetch review comments: ' . $e->getMessage()], 502);
        }

        $rows = array_map(fn($r) => [
            'document_id'    => $r['cr6f2_documentsid'],
            'document_name'  => $r['cr6f2_documentname'] ?? null,
            'main_doc_type'  => $r['cr6f2_maindoctype'] ?? null,
            'review_comment' => $r['cr6f2_reviewcomment'] ?? null,
            'document_link'  => $r['cr6f2_documentlink'] ?? null,
            'stored_link'    => $r['cr6f2_documentstoredlink'] ?? null,
            'modified_on'    => $r['modifiedon'] ?? null,
            'company_id'     => $r['_cr6f2_companyid_value'] ?? null,
            'company_name'   => $r['_cr6f2_companyid_value@OData.Community.Display.V1.FormattedValue'] ?? null,
            'modified_by'    => $r['_modifiedby_value@OData.Community.Display.V1.FormattedValue'] ?? null,
        ], $result['value'] ?? []);

        return response()->json(['success' => true, 'data' => $rows]);
    }

    // PATCH /api/v1/developer/feedback/{id}/status
    // Updates the row's status AND appends a single audit line to
    // cr6f2_feedbackhistory ("read-modify-write" — Dataverse doesn't support
    // append-only mutations). Most-recent entry sits at the TOP of the field.
    public function updateStatus(Request $request, string $id): JsonResponse
    {
        $data = $request->validate([
            'status' => ['required', 'integer', Rule::in(self::STATUS_VALUES)],
            'remark' => ['sometimes', 'nullable', 'string', 'max:1000'],
        ]);
        $newStatus = (int) $data['status'];
        $remark    = trim((string) ($data['remark'] ?? ''));

        // Read current status + history so we can record the transition.
        try {
            $current = $this->dv->find('cr6f2_userfeedbacks', $id, [
                'select' => ['cr6f2_status', 'cr6f2_feedbackhistory', 'cr6f2_message', '_cr6f2_submittedby_value', '_cr6f2_submittedbyappuser_value'],
            ]);
        } catch (Throwable $e) {
            $code = str_contains($e->getMessage(), '404') ? 404 : 502;
            return response()->json(['success' => false, 'message' => $code === 404 ? 'Feedback row not found.' : 'Failed to read feedback row.'], $code);
        }

        $fromStatus = (int) ($current['cr6f2_status'] ?? -1);
        $prevHist   = (string) ($current['cr6f2_feedbackhistory'] ?? '');

        // No-op short-circuit — don't write history when status didn't change.
        if ($fromStatus === $newStatus) {
            return response()->json(['success' => true, 'data' => ['id' => $id, 'status' => $newStatus]]);
        }

        $actor    = $request->user()?->email ?? $request->user()?->name ?? 'system';
        $when     = Carbon::now(self::REPORT_TZ)->format('Y-m-d H:i');
        $fromLbl  = self::STATUS_LABELS[$fromStatus] ?? "status {$fromStatus}";
        $toLbl    = self::STATUS_LABELS[$newStatus]  ?? "status {$newStatus}";
        $line     = "[{$when} {$actor}] {$fromLbl} → {$toLbl}";
        if ($remark !== '') {
            $line .= "\n  remark: {$remark}";
        }
        $newHist  = $prevHist === '' ? $line : "{$line}\n{$prevHist}";

        try {
            $this->dv->update('cr6f2_userfeedbacks', $id, [
                'cr6f2_status'          => $newStatus,
                'cr6f2_feedbackhistory' => $newHist,
            ]);
        } catch (Throwable $e) {
            $code = str_contains($e->getMessage(), '404') ? 404 : 502;
            return response()->json(['success' => false, 'message' => $code === 404 ? 'Feedback row not found.' : 'Failed to update status: ' . $e->getMessage()], $code);
        }

        // When resolved, drop a bell notification to the person who submitted
        // the feedback so they know it's been actioned. Best-effort — never
        // block the response on the notification write.
        if ($newStatus === 521880002) {
            // A self-registered submitter has no systemusers row, so SubmittedBy
            // was never set on their feedback — only SubmittedByAppUser was. Read
            // both, else they never get told their own feedback was resolved.
            $submitterGuid   = $current['_cr6f2_submittedby_value'] ?? null;
            $submitterAppUser = $current['_cr6f2_submittedbyappuser_value'] ?? null;
            if ($submitterGuid || $submitterAppUser) {
                try {
                    $msg     = (string) ($current['cr6f2_message'] ?? '');
                    $snippet = mb_substr($msg, 0, 80) . (mb_strlen($msg) > 80 ? '…' : '');
                    $logMsg  = $snippet !== ''
                        ? "Your feedback has been resolved — \"{$snippet}\""
                        : 'Your feedback has been resolved.';
                    if ($remark !== '') {
                        $logMsg .= ' · Resolution: ' . Str::limit($remark, 160);
                    }
                    $this->dv->create('cr6f2_systemlogs', [
                        'cr6f2_category'          => 521880008, // Feedback
                        'cr6f2_message'           => $logMsg,
                        'cr6f2_logtype'           => 521880000, // Info
                        'cr6f2_timestamp'         => now()->toIso8601String(),
                        ...($submitterAppUser
                            ? ['cr6f2_useridappuser@odata.bind' => '/' . config('identity.table') . "({$submitterAppUser})"]
                            : $this->appUsers->attributionBind('UserID', null, null, $submitterGuid)),
                    ]);
                } catch (Throwable $e) {
                    Log::warning('feedback resolve: submitter notification failed', ['feedback_id' => $id, 'error' => $e->getMessage()]);
                }
            }
        }

        return response()->json(['success' => true, 'data' => ['id' => $id, 'status' => $newStatus]]);
    }
}
