<?php

namespace App\Services\Admin\Qna;

use App\Repositories\Admin\Cs\AdminQnaRepository;
use App\Services\SmsService;
use App\Support\LegacyCrypto\CiSeedCrypto;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
use App\Jobs\SendUserQnaAnsweredMailJob;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Log;
use App\Services\Admin\AdminAuditService;

final class AdminQnaService
{
    public function __construct(
        private readonly AdminQnaRepository $repo,
        private readonly AdminAuditService $audit,
    ) {}

    public function stateLabels(): array
    {
        return [
            'a' => ['접수',     'pill--muted'], // 회색(대기/접수)
            'b' => ['분배완료', 'pill--info'],  // 파랑(분배/할당 완료)
            'c' => ['처리중',   'pill--warn'],  // 주황(진행중)
            'd' => ['보류',     'pill--bad'],   // 빨강(중단/보류)
            'e' => ['완료',     'pill--ok'],    // 초록(완료)
        ];
    }

    public function paginate(array $filters, int $perPage, int $adminId): array
    {
        $year = (int)($filters['year'] ?? date('Y'));
        if ($year <= 2000) $year = (int)date('Y');

        $filters['admin_id'] = $adminId;

        $rows = $this->repo->paginate($year, $filters, $perPage);

        if (is_object($rows) && method_exists($rows, 'withQueryString')) {
            $rows->withQueryString();
        }


        return [
            'year' => $year,
            'rows' => $this->repo->paginate($year, $filters, $perPage),
            'enquiryCodes' => $this->repo->getEnquiryCodes(),
            'stateLabels' => $this->stateLabels(),
            'categories' => array_values($this->categoriesByNum()),
            'categoriesMap' => $this->categoriesByNum(),
        ];
    }

    public function detail(int $seq, int $year, int $adminId): array
    {
        // 1) row
        $row = $this->repo->findOrFail($year, $seq);

        // 2) logs decode (state/memo)
        $log = $this->decodeJson((string)($row->admin_change_memo ?? ''));
        $stateLog = array_reverse($log['state_log'] ?? []);
        $memoLog  = array_reverse($log['memo'] ?? []);

        // 3) adminIds build (stateLog + memoLog + answer_admin_num) : set(연관배열)로 중복 제거
        $adminSet = [];

        foreach ($stateLog as $x) {
            $id = (int)($x['admin_num'] ?? 0);
            if ($id > 0) $adminSet[$id] = true;
        }
        foreach ($memoLog as $x) {
            $id = (int)($x['admin_num'] ?? 0);
            if ($id > 0) $adminSet[$id] = true;
        }

        $ans = (int)($row->answer_admin_num ?? 0);
        if ($ans > 0) $adminSet[$ans] = true;

        $adminIds = array_keys($adminSet);

        // 4) admins
        $admins = $adminIds ? $this->repo->getAdminsByIds($adminIds) : collect();

        // 5) member + decrypt phone
        $member = $this->repo->getMemberInfo((int)($row->member_num ?? 0));
        if ($member && !empty($member->cell_phone)) {
            $seed = app(\App\Support\LegacyCrypto\CiSeedCrypto::class);
            $member->cell_phone = (string) $seed->decrypt((string)$member->cell_phone);
        }

        // 6) 최근 거래 10건: 당분간 무조건 0건 (쿼리 절대 X)
        $orders = [];

        // 7) codes cache
        $enquiryCodes = \Cache::remember('cs:enquiry_codes', 3600, fn () => $this->repo->getEnquiryCodes());
        $deferCodes   = \Cache::remember('cs:defer_codes', 3600, fn () => $this->repo->getDeferCodes());

        // 8) actions + 내부 메모 권한(배정 후부터, 내 업무만)
        $actions = $this->allowedActions($row, $adminId);
        $state = (string)($row->state ?? '');
        $assignedToMe = ((int)($row->answer_admin_num ?? 0) === $adminId);
        $actions['canMemo'] = ($state !== 'a') && $assignedToMe; // a(접수) 금지, b/c/d/e OK

        // 9) categories (config/cs_faq.php 기반 num 매핑)
        $categoriesMap = $this->categoriesByNum();

        return [
            'row' => $row,
            'member' => $member,
            'orders' => $orders,
            'stateLabels' => $this->stateLabels(),
            'enquiryCodes' => $enquiryCodes,
            'deferCodes' => $deferCodes,
            'stateLog' => $stateLog,
            'memoLog' => $memoLog,
            'admins' => $admins,
            'actions' => $actions,
            'categories' => array_values($categoriesMap),
            'categoriesMap' => $categoriesMap,
        ];
    }


    public function assignToMe(int $seq, int $year, int $adminId): void
    {
        DB::transaction(function () use ($seq, $year, $adminId) {
            $row = $this->repo->lockForUpdate($year, $seq, [
                'seq','state','answer_admin_num','admin_change_memo'
            ]);

            if ($row->state !== 'a') {
                throw ValidationException::withMessages([
                    'answer_content' => '현재 상태에서는 배정할 수 없습니다.',
                ]);
            }

            // ✅ 감사로그 before (변경 전)
            $beforeAudit = [
                'year' => $year,
                'seq' => $seq,
                'state' => (string)($row->state ?? ''),
                'answer_admin_num' => (int)($row->answer_admin_num ?? 0),
            ];

            $log = $this->decodeJson((string)($row->admin_change_memo ?? ''));
            $log = $this->pushStateLog($log, $adminId, $row->state, 'b', '업무를 직접 배정');

            $payload = [
                'state' => 'b',
                'answer_admin_num' => $adminId,
                'receipt_date' => now()->format('Y-m-d H:i:s'),
                'admin_change_memo' => json_encode($log, JSON_UNESCAPED_UNICODE),
            ];

            $this->repo->update($year, $seq, $payload);

            // ✅ 감사로그 after (변경 후) — 감사로그 실패해도 본 로직 영향 X
            try {
                $req = request();

                $afterAudit = $beforeAudit;
                $afterAudit['state'] = 'b';
                $afterAudit['answer_admin_num'] = $adminId;
                $afterAudit['receipt_date'] = $payload['receipt_date'];

                $this->audit->log(
                    actorAdminId: $adminId,
                    action: 'admin.qna.assign_to_me',
                    targetType: 'qna',
                    targetId: (int)$seq, // year가 분리키면 before/after에 이미 남김
                    before: $beforeAudit,
                    after: $afterAudit,
                    ip: (string)($req?->ip() ?? ''),
                    ua: (string)($req?->userAgent() ?? ''),
                );
            } catch (\Throwable $ignored) {}
        });
    }


    public function startWork(int $seq, int $year, int $adminId): void
    {
        DB::transaction(function () use ($seq, $year, $adminId) {
            $row = $this->repo->lockForUpdate($year, $seq, [
                'seq','state','answer_admin_num','admin_change_memo'
            ]);

            $assigned = (int)($row->answer_admin_num ?? 0);
            if ($assigned !== $adminId) abort(403);

            if (!in_array($row->state, ['b','d'], true)) {
                throw ValidationException::withMessages([
                    'answer_content' => '현재 상태에서는 업무 시작이 불가합니다.',
                ]);
            }

            // ✅ 감사로그 before (변경 전)
            $beforeAudit = [
                'year' => $year,
                'seq' => $seq,
                'state' => (string)($row->state ?? ''),
                'answer_admin_num' => (int)($row->answer_admin_num ?? 0),
            ];

            $msg = ($row->state === 'd') ? '업무를 재개합니다!' : '업무를 시작했습니다.';
            $log = $this->decodeJson((string)($row->admin_change_memo ?? ''));
            $log = $this->pushStateLog($log, $adminId, $row->state, 'c', $msg);

            $this->repo->update($year, $seq, [
                'state' => 'c',
                'admin_change_memo' => json_encode($log, JSON_UNESCAPED_UNICODE),
            ]);

            // ✅ 감사로그 after (성공시에만 / 실패해도 본 로직 영향 X)
            try {
                $req = request();

                $afterAudit = $beforeAudit;
                $afterAudit['state'] = 'c';

                $this->audit->log(
                    actorAdminId: $adminId,
                    action: 'admin.qna.start_work',
                    targetType: 'qna',
                    targetId: (int)$seq,
                    before: $beforeAudit,
                    after: $afterAudit,
                    ip: (string)($req?->ip() ?? ''),
                    ua: (string)($req?->userAgent() ?? ''),
                );
            } catch (\Throwable $ignored) {}
        });
    }


    public function returnWork(int $seq, int $year, int $adminId): void
    {
        DB::transaction(function () use ($seq, $year, $adminId) {
            $row = $this->repo->lockForUpdate($year, $seq, [
                'seq','state','answer_admin_num','admin_change_memo'
            ]);

            $assigned = (int)($row->answer_admin_num ?? 0);
            if ($assigned !== $adminId) abort(403);

            if (!in_array($row->state, ['b','c','d'], true)) {
                throw ValidationException::withMessages([
                    'answer_content' => '현재 상태에서는 반납할 수 없습니다.',
                ]);
            }

            // ✅ 감사로그 before (변경 전)
            $beforeAudit = [
                'year' => $year,
                'seq' => $seq,
                'state' => (string)($row->state ?? ''),
                'answer_admin_num' => (int)($row->answer_admin_num ?? 0),
            ];

            $log = $this->decodeJson((string)($row->admin_change_memo ?? ''));
            $log = $this->pushStateLog($log, $adminId, $row->state, 'a', '업무를 종료/반납했습니다.');

            $this->repo->update($year, $seq, [
                'state' => 'a',
                'answer_admin_num' => null,
                'admin_change_memo' => json_encode($log, JSON_UNESCAPED_UNICODE),
            ]);

            // ✅ 감사로그 after (성공시에만 / 실패해도 본 로직 영향 X)
            try {
                $req = request();

                $afterAudit = $beforeAudit;
                $afterAudit['state'] = 'a';
                $afterAudit['answer_admin_num'] = null;

                $this->audit->log(
                    actorAdminId: $adminId,
                    action: 'admin.qna.return_work',
                    targetType: 'qna',
                    targetId: (int)$seq,
                    before: $beforeAudit,
                    after: $afterAudit,
                    ip: (string)($req?->ip() ?? ''),
                    ua: (string)($req?->userAgent() ?? ''),
                );
            } catch (\Throwable $ignored) {}
        });
    }

    public function postponeWork(int $seq, int $year, int $adminId, string $deferCode, string $comment): void
    {
        DB::transaction(function () use ($seq, $year, $adminId, $deferCode, $comment) {
            $row = $this->repo->lockForUpdate($year, $seq, [
                'seq','state','answer_admin_num','admin_change_memo'
            ]);

            $assigned = (int)($row->answer_admin_num ?? 0);
            if ($assigned !== $adminId) abort(403);

            if (!in_array($row->state, ['b','c'], true)) {
                throw ValidationException::withMessages([
                    'answer_content' => '현재 상태에서는 보류할 수 없습니다.',
                ]);
            }

            $comment = trim($comment);
            if ($comment === '') {
                throw ValidationException::withMessages([
                    'answer_content' => '보류 사유를 입력해 주세요.',
                ]);
            }

            // ✅ 감사로그 before (변경 전)
            $beforeAudit = [
                'year' => $year,
                'seq' => $seq,
                'state' => (string)($row->state ?? ''),
                'answer_admin_num' => (int)($row->answer_admin_num ?? 0),
            ];

            $log = $this->decodeJson((string)($row->admin_change_memo ?? ''));
            $log = $this->pushStateLog($log, $adminId, $row->state, 'd', $comment);

            $payload = [
                'state' => 'd',
                'defer_date' => now()->format('Y-m-d H:i:s'),
                'defer_comment' => $comment,
                'defer_admin_num' => $adminId,
                'defer_code' => $deferCode ?: null,
                'admin_change_memo' => json_encode($log, JSON_UNESCAPED_UNICODE),
            ];

            $this->repo->update($year, $seq, $payload);

            // ✅ 감사로그 after (성공시에만 / 실패해도 본 로직 영향 X)
            try {
                $req = request();

                $afterAudit = $beforeAudit;
                $afterAudit['state'] = 'd';
                $afterAudit['defer_date'] = $payload['defer_date'];
                $afterAudit['defer_admin_num'] = $adminId;
                $afterAudit['defer_code'] = $payload['defer_code'];
                // comment는 길거나 민감할 수 있으니 전체 대신 길이/해시만 남김(원문 필요하면 여기만 변경)
                $afterAudit['defer_comment_len'] = mb_strlen($comment);
                $afterAudit['defer_comment_sha256'] = hash('sha256', $comment);

                $this->audit->log(
                    actorAdminId: $adminId,
                    action: 'admin.qna.postpone_work',
                    targetType: 'qna',
                    targetId: (int)$seq,
                    before: $beforeAudit,
                    after: $afterAudit,
                    ip: (string)($req?->ip() ?? ''),
                    ua: (string)($req?->userAgent() ?? ''),
                );
            } catch (\Throwable $ignored) {}
        });
    }


    public function saveAnswer(int $seq, int $year, int $adminId, string $answerContent, string $answerSms): void
    {
        DB::transaction(function () use ($seq, $year, $adminId, $answerContent, $answerSms) {
            $row = $this->repo->lockForUpdate($year, $seq, [
                'seq','state','answer_admin_num','admin_change_memo'
            ]);
            $this->assertCanMemo($row, $adminId);

            $assigned = (int)($row->answer_admin_num ?? 0);
            if ($assigned !== $adminId) abort(403);

            if (!in_array($row->state, ['c'], true)) {
                throw ValidationException::withMessages([
                    'answer_content' => '처리중 상태에서만 답변 저장이 가능합니다.',
                ]);
            }

            $answerContent = trim($answerContent);
            $answerSms = trim($answerSms);

            if ($answerContent === '') abort(422, '관리자 답변을 입력해 주세요.');
            if ($answerSms === '') abort(422, 'SMS 답변을 입력해 주세요.');

            // ✅ 감사로그 before (변경 전) — 원문은 저장하지 않고 길이/해시만
            $beforeAudit = [
                'year' => $year,
                'seq' => $seq,
                'state' => (string)($row->state ?? ''),
                'answer_admin_num' => (int)($row->answer_admin_num ?? 0),
            ];

            $log = $this->decodeJson((string)($row->admin_change_memo ?? ''));
            $log = $this->pushMemoLog($log, $adminId, '관리자 답변 수정');

            $this->repo->update($year, $seq, [
                'answer_content' => $answerContent,
                'answer_sms' => $answerSms,
                'admin_change_memo' => json_encode($log, JSON_UNESCAPED_UNICODE),
            ]);

            // ✅ 감사로그 after (성공시에만 / 실패해도 본 로직 영향 X)
            try {
                $req = request();

                $afterAudit = $beforeAudit;
                $afterAudit['answer_content_len'] = mb_strlen($answerContent);
                $afterAudit['answer_content_sha256'] = hash('sha256', $answerContent);
                $afterAudit['answer_sms_len'] = mb_strlen($answerSms);
                $afterAudit['answer_sms_sha256'] = hash('sha256', $answerSms);

                $this->audit->log(
                    actorAdminId: $adminId,
                    action: 'admin.qna.save_answer',
                    targetType: 'qna',
                    targetId: (int)$seq,
                    before: $beforeAudit,
                    after: $afterAudit,
                    ip: (string)($req?->ip() ?? ''),
                    ua: (string)($req?->userAgent() ?? ''),
                );
            } catch (\Throwable $ignored) {}
        });
    }


    public function completeWork(int $seq, int $year, int $adminId): void
    {
        $notify = null;

        DB::transaction(function () use ($seq, $year, $adminId, &$notify) {
            $row = $this->repo->lockForUpdate($year, $seq, [
                'seq','state','answer_admin_num','admin_change_memo',
                'return_type','member_num','enquiry_title','enquiry_content',
                'answer_content','answer_sms','regdate',
            ]);

            $assigned = (int)($row->answer_admin_num ?? 0);
            if ($assigned !== $adminId) abort(403);

            if (!in_array($row->state, ['b','c'], true)) {
                throw ValidationException::withMessages([
                    'answer_content' => '현재 상태에서는 완료 처리가 불가합니다.',
                ]);
            }

            // ✅ 감사로그 before (변경 전)
            $beforeAudit = [
                'year' => $year,
                'seq' => $seq,
                'state' => (string)($row->state ?? ''),
                'answer_admin_num' => (int)($row->answer_admin_num ?? 0),
                'return_type' => (string)($row->return_type ?? 'web'),
                'member_num' => (int)($row->member_num ?? 0),
            ];

            $answerContent = trim((string)($row->answer_content ?? ''));
            $answerSms = trim((string)($row->answer_sms ?? ''));

            if ($answerContent === '') {
                throw ValidationException::withMessages([
                    'answer_content' => '답변 내용이 비어있습니다. 먼저 저장해 주세요.',
                ]);
            }

            $rt = (string)($row->return_type ?? 'web');
            if (in_array($rt, ['sms','phone'], true) && $answerSms === '') {
                abort(422, 'SMS 답변이 비어있습니다.');
            }

            $log = $this->decodeJson((string)($row->admin_change_memo ?? ''));
            $log = $this->pushStateLog($log, $adminId, $row->state, 'e', '업무를 완료했습니다.');

            $completionDate = now()->format('Y-m-d H:i:s');

            $this->repo->update($year, $seq, [
                'state' => 'e',
                'completion_date' => $completionDate,
                'admin_change_memo' => json_encode($log, JSON_UNESCAPED_UNICODE),
            ]);

            // ✅ 감사로그 after (성공시에만 / 실패해도 본 로직 영향 X)
            try {
                $req = request();

                $afterAudit = $beforeAudit;
                $afterAudit['state'] = 'e';
                $afterAudit['completion_date'] = $completionDate;

                // 답변 원문은 감사로그에 저장하지 않고 길이/해시만
                $afterAudit['answer_content_len'] = mb_strlen($answerContent);
                $afterAudit['answer_content_sha256'] = hash('sha256', $answerContent);
                $afterAudit['answer_sms_len'] = mb_strlen($answerSms);
                $afterAudit['answer_sms_sha256'] = hash('sha256', $answerSms);

                $this->audit->log(
                    actorAdminId: $adminId,
                    action: 'admin.qna.complete_work',
                    targetType: 'qna',
                    targetId: (int)$seq,
                    before: $beforeAudit,
                    after: $afterAudit,
                    ip: (string)($req?->ip() ?? ''),
                    ua: (string)($req?->userAgent() ?? ''),
                );
            } catch (\Throwable $ignored) {}

            // 트랜잭션 밖에서 발송하도록 데이터만 준비
            $notify = [
                'return_type' => $rt,
                'member_num' => (int)$row->member_num,
                'enquiry_title' => (string)$row->enquiry_title,
                'enquiry_content' => (string)$row->enquiry_content,
                'answer_content' => $answerContent,
                'answer_sms' => $answerSms,
                'regdate' => (string)$row->regdate,
                'year' => $year,
                'seq' => (int)$row->seq,
            ];
        });

        if (is_array($notify)) {
            DB::afterCommit(function () use ($notify) {
                $this->sendResultToUser($notify);
            });
        }
    }


    public function addMemo(int $seq, int $year, int $adminId, string $memo): void
    {
        DB::transaction(function () use ($seq, $year, $adminId, $memo) {
            $memo = trim($memo);
            if ($memo === '') abort(422);

            $row = $this->repo->lockForUpdate($year, $seq, [
                'seq','answer_admin_num','admin_change_memo'
            ]);

            // 메모는 배정자만 제한할지, 모든 관리자 가능할지 선택. 관리자 모두
            // if ((int)($row->answer_admin_num ?? 0) !== $adminId) abort(403);

            // ✅ 감사로그 before (변경 전) — 메모 원문은 저장하지 않음
            $beforeAudit = [
                'year' => $year,
                'seq' => $seq,
                'answer_admin_num' => (int)($row->answer_admin_num ?? 0),
            ];

            $log = $this->decodeJson((string)($row->admin_change_memo ?? ''));
            $log = $this->pushMemoLog($log, $adminId, $memo);

            $this->repo->update($year, $seq, [
                'admin_change_memo' => json_encode($log, JSON_UNESCAPED_UNICODE),
            ]);

            // ✅ 감사로그 after (성공시에만 / 실패해도 본 로직 영향 X)
            try {
                $req = request();

                $afterAudit = $beforeAudit;
                $afterAudit['memo_len'] = mb_strlen($memo);
                $afterAudit['memo_sha256'] = hash('sha256', $memo);

                $this->audit->log(
                    actorAdminId: $adminId,
                    action: 'admin.qna.memo.add',
                    targetType: 'qna',
                    targetId: (int)$seq,
                    before: $beforeAudit,
                    after: $afterAudit,
                    ip: (string)($req?->ip() ?? ''),
                    ua: (string)($req?->userAgent() ?? ''),
                );
            } catch (\Throwable $ignored) {}
        });
    }


    private function allowedActions(object $row, int $adminId): array
    {
        $state = (string)$row->state;
        $assigned = (int)($row->answer_admin_num ?? 0);

        return [
            'can_assign' => ($state === 'a'),
            'can_start' => ($assigned === $adminId && in_array($state, ['b','d'], true)),
            'can_return' => ($assigned === $adminId && in_array($state, ['b','c','d'], true)),
            'can_postpone' => ($assigned === $adminId && in_array($state, ['b','c'], true)),
            'can_answer' => ($assigned === $adminId && $state === 'c'),
            'can_complete' => ($assigned === $adminId && in_array($state, ['b','c'], true)),
        ];
    }

    private function decodeJson(string $json): array
    {
        $json = trim($json);
        if ($json === '') return [];
        $arr = json_decode($json, true);
        return is_array($arr) ? $arr : [];
    }

    private function pushStateLog(array $log, int $adminId, string $before, string $after, string $why): array
    {
        $log['state_log'] ??= [];
        $log['state_log'][] = [
            'admin_num' => $adminId,
            'state_before' => $before,
            'state_after' => $after,
            'Who' => 'admin',
            'why' => $why,
            'when' => now()->format('y-m-d H:i:s'),
        ];
        return $log;
    }

    private function pushMemoLog(array $log, int $adminId, string $memo): array
    {
        $log['memo'] ??= [];
        $log['memo'][] = [
            'admin_num' => $adminId,
            'memo' => $memo,
            'when' => now()->format('y-m-d H:i:s'),
        ];
        return $log;
    }

    private function maskEmail(string $email): string
    {
        $email = trim($email);
        if ($email === '' || !str_contains($email, '@')) return '';
        [$id, $dom] = explode('@', $email, 2);
        $idLen = mb_strlen($id);
        if ($idLen <= 2) return str_repeat('*', $idLen) . '@' . $dom;
        return mb_substr($id, 0, 2) . str_repeat('*', max(1, $idLen - 2)) . '@' . $dom;
    }

    private function maskPhone(string $digits): string
    {
        $digits = preg_replace('/\D+/', '', $digits) ?? '';
        $len = strlen($digits);
        if ($len <= 4) return str_repeat('*', $len);
        return substr($digits, 0, 3) . str_repeat('*', max(0, $len - 7)) . substr($digits, -4);
    }

    private function sendResultToUser(array $data): void
    {
        $memberNum = (int)($data['member_num'] ?? 0);
        $rt = (string)($data['return_type'] ?? 'web');
        $year = (int)($data['year'] ?? date('Y'));
        $qnaId = (int)($data['seq'] ?? $data['qna_id'] ?? 0);

        Log::info('[QNA][result] enter', [
            'member_num' => $memberNum,
            'qna_id' => $qnaId,
            'year' => $year,
            'return_type' => $rt,
            'has_answer_content' => (trim((string)($data['answer_content'] ?? '')) !== ''),
            'answer_sms_len' => mb_strlen((string)($data['answer_sms'] ?? '')),
        ]);

        if ($memberNum <= 0 || $qnaId <= 0) {
            Log::warning('[QNA][result] invalid keys', [
                'member_num' => $memberNum,
                'qna_id' => $qnaId,
            ]);
            return;
        }

        $member = $this->repo->getMemberInfo($memberNum);
        if (!$member) {
            Log::warning('[QNA][result] member not found', [
                'member_num' => $memberNum,
            ]);
            return;
        }

        // EMAIL
        if ($rt === 'email') {
            $to = trim((string)($member->email ?? ''));
            if ($to === '') {
                Log::warning('[QNA][result][email] no email', [
                    'member_num' => $memberNum,
                    'qna_id' => $qnaId,
                ]);
                return;
            }

            Log::info('[QNA][result][email] dispatching', [
                'to' => $this->maskEmail($to),
                'connection' => 'redis',
                'queue' => 'mail',
                'after_commit' => true,
            ]);

            SendUserQnaAnsweredMailJob::dispatch(
                $to,
                $memberNum,
                $qnaId,
                [
                    'year' => $year,
                    'enquiry_code' => (string)($data['enquiry_code'] ?? ''),
                    'enquiry_title' => (string)($data['enquiry_title'] ?? ''),
                    'enquiry_content' => (string)($data['enquiry_content'] ?? ''),
                    'answer_content' => (string)($data['answer_content'] ?? ''),
                    'answer_sms' => (string)($data['answer_sms'] ?? ''),
                    'regdate' => (string)($data['regdate'] ?? ''),
                    'completed_at' => (string)($data['completed_at'] ?? now()->toDateTimeString()),
                ]
            )
                ->onConnection('redis')
                ->onQueue('mail')
                ->afterCommit();

            Log::info('[QNA][result][email] dispatched', [
                'to' => $this->maskEmail($to),
                'member_num' => $memberNum,
                'qna_id' => $qnaId,
            ]);

            return;
        }

        // SMS
        if (in_array($rt, ['sms', 'phone'], true)) {
            $msg = trim((string)($data['answer_sms'] ?? ''));
            if ($msg === '') {
                Log::warning('[QNA][result][sms] empty answer_sms', [
                    'member_num' => $memberNum,
                    'qna_id' => $qnaId,
                ]);
                return;
            }

            $phoneEnc = (string)($member->cell_phone ?? '');
            if ($phoneEnc === '') {
                Log::warning('[QNA][result][sms] empty cell_phone', [
                    'member_num' => $memberNum,
                    'qna_id' => $qnaId,
                ]);
                return;
            }

            try {
                $seed = app(CiSeedCrypto::class);
                $phonePlain = (string)$seed->decrypt($phoneEnc);
                $digits = preg_replace('/\D+/', '', $phonePlain) ?? '';
                if ($digits === '') {
                    Log::warning('[QNA][result][sms] decrypt ok but no digits', [
                        'member_num' => $memberNum,
                        'qna_id' => $qnaId,
                    ]);
                    return;
                }

                Log::info('[QNA][result][sms] sending', [
                    'to' => $this->maskPhone($digits),
                    'msg_len' => mb_strlen($msg),
                ]);

                app(SmsService::class)->send([
                    'from_number' => '1833-4856',
                    'to_number'   => $digits,
                    'message'     => $msg,
                    'sms_type'    => 'sms',
                    'country'     => 82,
                ]);

                Log::info('[QNA][result][sms] sent', [
                    'to' => $this->maskPhone($digits),
                ]);

            } catch (\Throwable $e) {
                Log::warning('[QNA][result][sms] failed', [
                    'member_num' => $memberNum,
                    'qna_id' => $qnaId,
                    'err' => $e->getMessage(),
                ]);
            }
        }
    }

    private function categoriesByNum(): array
    {
        $cats = (array) config('cs_faq.categories', []);
        $map = [];
        foreach ($cats as $c) {
            $num = (int)($c['num'] ?? 0);
            if ($num > 0) $map[$num] = $c; // ['key','label','num']
        }
        return $map;
    }

    private function categoryLabel($enquiryCode): string
    {
        $num = (int) $enquiryCode;
        $map = $this->categoriesByNum();
        return $map[$num]['label'] ?? ((string)$enquiryCode ?: '-');
    }

    private function assertCanMemo(object $row, int $adminId): void
    {
        if ((string)($row->state ?? '') === 'a') {
            throw ValidationException::withMessages([
                'answer_content' => '업무 배정 후 메모를 남길 수 있습니다.',
            ]);
        }
        if ((int)($row->answer_admin_num ?? 0) !== $adminId) {
            throw ValidationException::withMessages([
                'answer_content' => '내 업무로 배정된 건만 메모를 남길 수 있습니다.',
            ]);
        }
    }

}
