<?php

namespace App\Http\Controllers\ESign;

use App\Http\Controllers\Controller;
use App\Mail\EsignCompletedMail;
use App\Mail\EsignRequestMail;
use App\Models\Barobill\BarobillMember;
use App\Models\ESign\EsignAuditLog;
use App\Models\ESign\EsignContract;
use App\Models\ESign\EsignSigner;
use App\Models\Tenants\TenantSetting;
use App\Services\Barobill\BarobillService;
use App\Services\ESign\PdfSignatureService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\View\View;
use Symfony\Component\HttpFoundation\StreamedResponse;

class EsignPublicController extends Controller
{
    // ─── 전자계약 서명 확인 (전화번호 기반) ───

    public function verifyPhone(Request $request)
    {
        $phone = preg_replace('/[^0-9]/', '', $request->input('phone', ''));

        if (strlen($phone) < 10) {
            return back()->withErrors(['phone' => '올바른 전화번호를 입력해 주세요.']);
        }

        $signer = EsignSigner::withoutGlobalScopes()
            ->where('phone', $phone)
            ->whereIn('status', ['pending', 'notified'])
            ->whereHas('contract', fn ($q) => $q->where('status', 'pending'))
            ->latest('created_at')
            ->first();

        if (! $signer) {
            return back()->withErrors(['phone' => '대기 중인 전자계약이 없습니다.']);
        }

        return redirect("/esign/sign/{$signer->access_token}");
    }

    // ─── 화면 라우트 ───

    public function auth(string $token): View
    {
        return view('esign.sign.auth', ['token' => $token]);
    }

    public function sign(string $token): View
    {
        return view('esign.sign.sign', ['token' => $token]);
    }

    public function done(string $token): View
    {
        return view('esign.sign.done', ['token' => $token]);
    }

    // ─── 서명 API (토큰 기반, 비인증) ───

    /**
     * 토큰으로 계약/서명자 정보 조회
     */
    public function getContract(string $token): JsonResponse
    {
        $signer = EsignSigner::withoutGlobalScopes()
            ->where('access_token', $token)
            ->first();

        if (! $signer) {
            return response()->json(['success' => false, 'message' => '유효하지 않은 서명 링크입니다.'], 404);
        }

        if ($signer->token_expires_at && $signer->token_expires_at->isPast()) {
            return response()->json(['success' => false, 'message' => '서명 링크가 만료되었습니다.'], 400);
        }

        $contract = EsignContract::withoutGlobalScopes()
            ->with(['signers:id,contract_id,name,role,status,signed_at'])
            ->find($signer->contract_id);

        if (! $contract) {
            return response()->json(['success' => false, 'message' => '계약을 찾을 수 없습니다.'], 404);
        }

        // 서명 가능 여부 판별
        $isSignable = in_array($contract->status, ['pending', 'partially_signed'])
            && in_array($signer->status, ['notified', 'viewing', 'authenticated']);

        $statusMessage = null;
        $unsignableStatuses = [
            'draft' => '아직 발송되지 않은 계약입니다.',
            'cancelled' => '취소된 계약입니다.',
            'rejected' => '거절된 계약입니다.',
        ];
        if (isset($unsignableStatuses[$contract->status])) {
            $statusMessage = $unsignableStatuses[$contract->status];
        } elseif ($signer->status === 'signed') {
            $statusMessage = '이미 서명을 완료하였습니다.';
        }

        // 서명 가능한 상태에서만 감사 로그 기록
        if ($isSignable) {
            EsignAuditLog::create([
                'tenant_id' => $contract->tenant_id,
                'contract_id' => $contract->id,
                'signer_id' => $signer->id,
                'action' => 'viewed',
                'ip_address' => request()->ip(),
                'user_agent' => request()->userAgent(),
                'created_at' => now(),
            ]);
        }

        return response()->json([
            'success' => true,
            'data' => [
                'contract' => $contract,
                'signer' => [
                    'id' => $signer->id,
                    'name' => $signer->name,
                    'email' => $signer->email,
                    'phone' => $signer->phone,
                    'role' => $signer->role,
                    'status' => $signer->status,
                    'has_stamp' => (bool) $signer->signature_image_path || ($signer->role === 'creator' && $this->hasCompanyStamp($contract->tenant_id)),
                    'signed_at' => $signer->signed_at,
                ],
                'send_method' => $contract->tenant_id == 1 ? 'email' : ($contract->send_method ?? 'email'),
                'is_signable' => $isSignable,
                'status_message' => $statusMessage,
            ],
        ]);
    }

    /**
     * OTP 발송
     */
    public function sendOtp(string $token): JsonResponse
    {
        $signer = $this->findSigner($token);
        if (! $signer) {
            return response()->json(['success' => false, 'message' => '유효하지 않은 서명 링크입니다.'], 404);
        }

        $contract = EsignContract::withoutGlobalScopes()->find($signer->contract_id);
        if (! $contract || ! in_array($contract->status, ['pending', 'partially_signed'])) {
            return response()->json(['success' => false, 'message' => '서명할 수 없는 상태입니다.'], 400);
        }

        $otpCode = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);

        $signer->update([
            'otp_code' => $otpCode,
            'otp_expires_at' => now()->addMinutes(5),
            'otp_attempts' => 0,
        ]);

        $sendMethod = $contract->send_method ?? 'email';
        $channel = 'email';

        // 알림톡/both 방식이고 전화번호가 있으면 SMS로 발송 (상대방만 SMS, 본사(creator)는 이메일 유지)
        if (in_array($sendMethod, ['alimtalk', 'both']) && $signer->phone && $signer->role === EsignSigner::ROLE_COUNTERPART) {
            $smsSent = $this->sendOtpViaSms($contract, $signer, $otpCode);
            if ($smsSent) {
                $channel = 'sms';
            } else {
                // SMS 실패 시 이메일 폴백
                if ($signer->email) {
                    Mail::to($signer->email)->send(new \App\Mail\EsignOtpMail($signer->name, $otpCode));
                    $channel = 'email';
                } else {
                    return response()->json(['success' => false, 'message' => 'OTP 발송에 실패했습니다.'], 500);
                }
            }
        } else {
            // 이메일 방식 또는 전화번호 없음
            Mail::to($signer->email)->send(new \App\Mail\EsignOtpMail($signer->name, $otpCode));
            $channel = 'email';
        }

        EsignAuditLog::create([
            'tenant_id' => $contract->tenant_id,
            'contract_id' => $contract->id,
            'signer_id' => $signer->id,
            'action' => 'otp_sent',
            'ip_address' => request()->ip(),
            'user_agent' => request()->userAgent(),
            'metadata' => [
                'channel' => $channel,
                'email' => $channel === 'email' ? $signer->email : null,
                'phone' => $channel === 'sms' ? $signer->phone : null,
            ],
            'created_at' => now(),
        ]);

        return response()->json([
            'success' => true,
            'message' => '인증 코드가 발송되었습니다.',
            'channel' => $channel,
        ]);
    }

    /**
     * OTP 검증
     */
    public function verifyOtp(Request $request, string $token): JsonResponse
    {
        $signer = $this->findSigner($token);
        if (! $signer) {
            return response()->json(['success' => false, 'message' => '유효하지 않은 서명 링크입니다.'], 404);
        }

        if ($signer->otp_attempts >= 5) {
            return response()->json(['success' => false, 'message' => '인증 시도 횟수를 초과했습니다.'], 400);
        }

        if (! $signer->otp_code || ! $signer->otp_expires_at) {
            return response()->json(['success' => false, 'message' => '인증 코드가 발송되지 않았습니다.'], 400);
        }

        if ($signer->otp_expires_at->isPast()) {
            return response()->json(['success' => false, 'message' => '인증 코드가 만료되었습니다.'], 400);
        }

        $signer->increment('otp_attempts');

        $otpCode = $request->input('otp_code');
        if ($signer->otp_code !== $otpCode) {
            return response()->json(['success' => false, 'message' => '인증 코드가 올바르지 않습니다.'], 400);
        }

        $contract = EsignContract::withoutGlobalScopes()->find($signer->contract_id);

        $signer->update([
            'auth_verified_at' => now(),
            'otp_code' => null,
            'otp_expires_at' => null,
            'status' => 'authenticated',
        ]);

        EsignAuditLog::create([
            'tenant_id' => $contract->tenant_id,
            'contract_id' => $contract->id,
            'signer_id' => $signer->id,
            'action' => 'authenticated',
            'ip_address' => request()->ip(),
            'user_agent' => request()->userAgent(),
            'created_at' => now(),
        ]);

        return response()->json([
            'success' => true,
            'data' => ['sign_session_token' => Str::random(64)],
        ]);
    }

    /**
     * 서명 제출
     */
    public function submitSignature(Request $request, string $token): JsonResponse
    {
        $signer = $this->findSigner($token);
        if (! $signer) {
            return response()->json(['success' => false, 'message' => '유효하지 않은 서명 링크입니다.'], 404);
        }

        if (! in_array($signer->status, ['authenticated', 'viewing'])) {
            return response()->json(['success' => false, 'message' => '서명할 수 없는 상태입니다.'], 400);
        }

        $contract = EsignContract::withoutGlobalScopes()->find($signer->contract_id);
        if (! $contract || ! in_array($contract->status, ['pending', 'partially_signed'])) {
            return response()->json(['success' => false, 'message' => '서명할 수 없는 계약 상태입니다.'], 400);
        }

        $useStamp = $request->boolean('use_stamp');

        if ($useStamp) {
            $stampImage = $request->input('stamp_image');
            if ($stampImage) {
                // 고객 업로드 도장: base64 디코딩 후 저장
                $imageData = base64_decode($stampImage);
                if (strlen($imageData) > 5 * 1024 * 1024) {
                    return response()->json(['success' => false, 'message' => '도장 이미지는 5MB 이하만 가능합니다.'], 422);
                }
                $imagePath = "esign/{$contract->tenant_id}/signatures/{$contract->id}_{$signer->id}_stamp_".time().'.png';
                Storage::disk('local')->put($imagePath, $imageData);
                $signer->update(['signature_image_path' => $imagePath]);
            } elseif (! $signer->signature_image_path) {
                // 기존 계약: tenant_settings에서 법인도장 가져오기
                if ($signer->role === 'creator') {
                    $stampPath = $this->applyCompanyStamp($signer, $contract->tenant_id);
                    if (! $stampPath) {
                        return response()->json(['success' => false, 'message' => '등록된 도장 이미지가 없습니다.'], 422);
                    }
                } else {
                    return response()->json(['success' => false, 'message' => '등록된 도장 이미지가 없습니다.'], 422);
                }
            }
        } else {
            // 직접 서명: signature_image 필수
            $signatureImage = $request->input('signature_image');
            if (! $signatureImage) {
                return response()->json(['success' => false, 'message' => '서명 이미지가 필요합니다.'], 422);
            }

            // 서명 이미지 저장
            $imageData = base64_decode($signatureImage);
            $imagePath = "esign/{$contract->tenant_id}/signatures/{$contract->id}_{$signer->id}_".time().'.png';
            Storage::disk('local')->put($imagePath, $imageData);

            $signer->update(['signature_image_path' => $imagePath]);
        }

        // 서명자 상태 업데이트
        $signer->update([
            'signed_at' => now(),
            'consent_agreed_at' => now(),
            'sign_ip_address' => $request->ip(),
            'sign_user_agent' => $request->userAgent(),
            'status' => 'signed',
        ]);

        // 감사 로그
        EsignAuditLog::create([
            'tenant_id' => $contract->tenant_id,
            'contract_id' => $contract->id,
            'signer_id' => $signer->id,
            'action' => 'signed',
            'ip_address' => $request->ip(),
            'user_agent' => $request->userAgent(),
            'created_at' => now(),
        ]);

        // 모든 서명자가 서명 완료했는지 확인
        $allSigners = EsignSigner::withoutGlobalScopes()
            ->where('contract_id', $contract->id)
            ->get();
        $allSigned = $allSigners->every(fn ($s) => $s->status === 'signed');

        if ($allSigned) {
            $contract->update([
                'status' => 'completed',
                'completed_at' => now(),
            ]);

            // PDF에 서명 이미지/텍스트 합성
            try {
                $pdfService = new PdfSignatureService;
                $pdfService->mergeSignatures($contract);
            } catch (\Throwable $e) {
                Log::error('PDF 서명 합성 실패', [
                    'contract_id' => $contract->id,
                    'error' => $e->getMessage(),
                    'trace' => $e->getTraceAsString(),
                ]);
            }

            // 계약 완료 감사 로그
            EsignAuditLog::create([
                'tenant_id' => $contract->tenant_id,
                'contract_id' => $contract->id,
                'signer_id' => $signer->id,
                'action' => 'contract_completed',
                'ip_address' => $request->ip(),
                'user_agent' => $request->userAgent(),
                'metadata' => ['total_signers' => $allSigners->count()],
                'created_at' => now(),
            ]);

            // 모든 서명자에게 완료 알림 발송
            $sendMethod = $contract->send_method ?? 'alimtalk';
            foreach ($allSigners as $completedSigner) {
                $completionResults = [];
                try {
                    // 본사(creator): 이메일로 완료 알림
                    // 상대방(counterpart): 알림톡(카카오톡) + PDF 다운로드 링크
                    $isCounterpart = $completedSigner->role === EsignSigner::ROLE_COUNTERPART;

                    // 이메일 발송 조건:
                    // 1) email/both 선택 시
                    // 2) 본사(creator)는 항상 이메일
                    // 3) 상대방이지만 전화번호 없으면 이메일 폴백
                    $shouldSendEmail = in_array($sendMethod, ['email', 'both'])
                        || ! $isCounterpart
                        || ($isCounterpart && ! $completedSigner->phone);

                    if ($shouldSendEmail && $completedSigner->email) {
                        try {
                            Mail::to($completedSigner->email)->send(
                                new EsignCompletedMail($contract, $completedSigner, $allSigners)
                            );
                            $completionResults[] = ['success' => true, 'channel' => 'email', 'error' => null];
                        } catch (\Throwable $e) {
                            $completionResults[] = ['success' => false, 'channel' => 'email', 'error' => $e->getMessage()];
                        }
                    }

                    // 알림톡 발송: 상대방(counterpart)에게 카카오톡으로 서명 완료 PDF 전달
                    if (in_array($sendMethod, ['alimtalk', 'both']) && $isCounterpart && $completedSigner->phone) {
                        $alimtalkResult = $this->sendCompletionAlimtalk($contract, $completedSigner);
                        $completionResults[] = $alimtalkResult;

                        // 알림톡 실패 시 이메일 폴백 (아직 이메일 안 보낸 경우)
                        if (! ($alimtalkResult['success'] ?? false) && ! $shouldSendEmail && $completedSigner->email) {
                            try {
                                Mail::to($completedSigner->email)->send(
                                    new EsignCompletedMail($contract, $completedSigner, $allSigners)
                                );
                                $completionResults[] = ['success' => true, 'channel' => 'email', 'error' => null];
                            } catch (\Throwable $e) {
                                $completionResults[] = ['success' => false, 'channel' => 'email', 'error' => $e->getMessage()];
                            }
                        }
                    }

                    EsignAuditLog::create([
                        'tenant_id' => $contract->tenant_id,
                        'contract_id' => $contract->id,
                        'signer_id' => $completedSigner->id,
                        'action' => 'completion_notification_sent',
                        'ip_address' => $request->ip(),
                        'user_agent' => $request->userAgent(),
                        'metadata' => [
                            'send_method' => $sendMethod,
                            'signer_role' => $completedSigner->role,
                            'notification_results' => [[
                                'signer_id' => $completedSigner->id,
                                'signer_name' => $completedSigner->name,
                                'results' => $completionResults,
                            ]],
                        ],
                        'created_at' => now(),
                    ]);
                } catch (\Throwable $e) {
                    Log::error('계약 완료 알림 발송 실패', [
                        'contract_id' => $contract->id,
                        'signer_id' => $completedSigner->id,
                        'error' => $e->getMessage(),
                    ]);
                }
            }
        } else {
            $contract->update(['status' => 'partially_signed']);

            // 다음 서명자에게 자동 알림 발송 (순차 서명)
            $nextSigner = EsignSigner::withoutGlobalScopes()
                ->where('contract_id', $contract->id)
                ->whereIn('status', ['waiting', 'pending'])
                ->orderBy('sign_order')
                ->first();

            if ($nextSigner) {
                $nextSigner->update(['status' => 'notified']);
                $nextSendMethod = $contract->send_method ?? 'alimtalk';
                $nextSmsFallback = $contract->sms_fallback ?? true;
                $nextIsCounterpart = $nextSigner->role === EsignSigner::ROLE_COUNTERPART;

                $notificationResults = [];
                $alimtalkFailed = false;

                // 알림톡 발송: 상대방(counterpart)에게만 카카오톡 발송
                if (in_array($nextSendMethod, ['alimtalk', 'both']) && $nextIsCounterpart && $nextSigner->phone) {
                    try {
                        $member = BarobillMember::where('tenant_id', $contract->tenant_id)->first();
                        if ($member && $member->biz_no) {
                            $barobill = app(BarobillService::class);
                            $barobill->setServerMode($member->server_mode ?? 'production');
                            $nextSignUrl = config('app.url').'/esign/sign/'.$nextSigner->access_token;
                            $nextExpires = $contract->expires_at?->format('Y-m-d H:i') ?? '없음';

                            // 채널 ID 조회
                            $channelId = $this->getKakaotalkChannelId($barobill, $member->biz_no);

                            if ($channelId) {
                                // 템플릿 본문 + 버튼 조회
                                $nextTemplateName = $this->resolveTemplateName('전자계약_서명요청');
                                $tplData = $this->getTemplateData($barobill, $member->biz_no, $channelId, $nextTemplateName);
                                $tplMessage = $tplData['content']
                                    ? str_replace(
                                        ['#{이름}', '#{계약명}', '#{기한}'],
                                        [$nextSigner->name, $contract->title, $nextExpires],
                                        $tplData['content']
                                    )
                                    : null;

                                // 버튼: 템플릿에서 가져온 URL의 #{토큰} 치환
                                $buttons = ! empty($tplData['buttons']) ? $tplData['buttons'] : [
                                    ['Name' => '계약서 확인하기', 'ButtonType' => 'WL', 'Url1' => $nextSignUrl, 'Url2' => $nextSignUrl],
                                ];
                                foreach ($buttons as &$btn) {
                                    foreach (['Url1', 'Url2'] as $urlKey) {
                                        if (! empty($btn[$urlKey])) {
                                            $btn[$urlKey] = str_replace(
                                                ['#{토큰}', '#{%ED%86%A0%ED%81%B0}'],
                                                [$nextSigner->access_token, $nextSigner->access_token],
                                                urldecode($btn[$urlKey])
                                            );
                                        }
                                    }
                                }
                                unset($btn);

                                $atResult = $barobill->sendATKakaotalkEx(
                                    corpNum: $member->biz_no,
                                    senderId: $member->barobill_id,
                                    yellowId: $channelId,
                                    templateName: $nextTemplateName,
                                    receiverName: $nextSigner->name,
                                    receiverNum: preg_replace('/[^0-9]/', '', $nextSigner->phone),
                                    title: '',
                                    message: $tplMessage ?? "안녕하세요, {$nextSigner->name}님.\n전자계약 서명 요청이 도착했습니다.\n\n■ 계약명: {$contract->title}\n■ 서명 기한: {$nextExpires}\n\n아래 버튼을 눌러 계약서를 확인하고 서명해 주세요.",
                                    buttons: $buttons,
                                    smsMessage: $nextSmsFallback ? "[SAM] {$nextSigner->name}님, 전자계약 서명 요청이 도착했습니다. {$nextSignUrl}" : '',
                                );
                                $alimtalkFailed = ! ($atResult['success'] ?? false);
                                $notificationResults[] = [
                                    'success' => $atResult['success'] ?? false,
                                    'channel' => 'alimtalk',
                                    'error' => $atResult['error'] ?? null,
                                ];
                            } else {
                                $alimtalkFailed = true;
                                $notificationResults[] = ['success' => false, 'channel' => 'alimtalk', 'error' => '등록된 카카오톡 채널 없음'];
                            }
                        } else {
                            $alimtalkFailed = true;
                            $notificationResults[] = ['success' => false, 'channel' => 'alimtalk', 'error' => '바로빌 회원 미등록'];
                        }
                    } catch (\Throwable $e) {
                        Log::warning('다음 서명자 알림톡 발송 실패', ['error' => $e->getMessage()]);
                        $alimtalkFailed = true;
                        $notificationResults[] = ['success' => false, 'channel' => 'alimtalk', 'error' => $e->getMessage()];
                    }
                }

                // 이메일 발송 조건:
                // 1) email/both 선택 시
                // 2) 본사(creator)는 항상 이메일
                // 3) 상대방이지만 전화번호 없으면 이메일 폴백
                // 4) 알림톡 발송 실패 시 이메일 자동 폴백
                $shouldSendEmail = in_array($nextSendMethod, ['email', 'both'])
                    || ! $nextIsCounterpart
                    || ($nextSendMethod === 'alimtalk' && $nextIsCounterpart && ! $nextSigner->phone)
                    || ($nextSendMethod === 'alimtalk' && $nextIsCounterpart && $alimtalkFailed);

                if ($shouldSendEmail && $nextSigner->email) {
                    try {
                        Mail::to($nextSigner->email)->send(new EsignRequestMail($contract, $nextSigner));
                        $notificationResults[] = ['success' => true, 'channel' => 'email', 'error' => null];
                    } catch (\Throwable $e) {
                        Log::warning('다음 서명자 이메일 발송 실패', ['error' => $e->getMessage()]);
                        $notificationResults[] = ['success' => false, 'channel' => 'email', 'error' => $e->getMessage()];
                    }
                }

                EsignAuditLog::create([
                    'tenant_id' => $contract->tenant_id,
                    'contract_id' => $contract->id,
                    'signer_id' => $nextSigner->id,
                    'action' => 'sign_request_sent',
                    'ip_address' => $request->ip(),
                    'user_agent' => $request->userAgent(),
                    'metadata' => [
                        'triggered_by' => 'auto_after_sign',
                        'signer_role' => $nextSigner->role,
                        'notification_results' => [[
                            'signer_id' => $nextSigner->id,
                            'signer_name' => $nextSigner->name,
                            'results' => $notificationResults,
                        ]],
                    ],
                    'created_at' => now(),
                ]);
            }
        }

        return response()->json(['success' => true, 'message' => '서명이 완료되었습니다.']);
    }

    /**
     * 계약 거절
     */
    public function rejectContract(Request $request, string $token): JsonResponse
    {
        $signer = $this->findSigner($token);
        if (! $signer) {
            return response()->json(['success' => false, 'message' => '유효하지 않은 서명 링크입니다.'], 404);
        }

        $reason = $request->input('reason', '');
        $contract = EsignContract::withoutGlobalScopes()->find($signer->contract_id);

        $signer->update([
            'status' => 'rejected',
            'rejected_reason' => $reason,
        ]);

        $contract->update(['status' => 'rejected']);

        EsignAuditLog::create([
            'tenant_id' => $contract->tenant_id,
            'contract_id' => $contract->id,
            'signer_id' => $signer->id,
            'action' => 'rejected',
            'ip_address' => $request->ip(),
            'user_agent' => $request->userAgent(),
            'metadata' => ['reason' => $reason],
            'created_at' => now(),
        ]);

        return response()->json(['success' => true, 'message' => '서명이 거절되었습니다.']);
    }

    /**
     * 계약 문서 다운로드
     */
    public function downloadDocument(string $token): StreamedResponse|JsonResponse
    {
        $signer = $this->findSigner($token);
        if (! $signer) {
            return response()->json(['success' => false, 'message' => '유효하지 않은 서명 링크입니다.'], 404);
        }

        $contract = EsignContract::withoutGlobalScopes()->find($signer->contract_id);
        if (! $contract || ! $contract->original_file_path) {
            return response()->json(['success' => false, 'message' => '문서를 찾을 수 없습니다.'], 404);
        }

        // 서명 완료된 PDF가 있으면 우선 제공
        if ($contract->signed_file_path && Storage::disk('local')->exists($contract->signed_file_path)) {
            $filePath = $contract->signed_file_path;
        } elseif ($contract->status === 'completed') {
            // 계약 완료 상태인데 서명 PDF가 없으면 재생성 시도
            try {
                $pdfService = new PdfSignatureService;
                $filePath = $pdfService->mergeSignatures($contract);
                Log::info('서명 PDF 재생성 성공', ['contract_id' => $contract->id, 'path' => $filePath]);
            } catch (\Throwable $e) {
                Log::error('서명 PDF 재생성 실패', [
                    'contract_id' => $contract->id,
                    'error' => $e->getMessage(),
                    'trace' => $e->getTraceAsString(),
                ]);
                // 재생성 실패 시 미리보기 PDF 폴백 (서명 제외, 텍스트/날짜/체크박스만)
                try {
                    $filePath = $pdfService->generatePreview($contract);
                } catch (\Throwable $e2) {
                    Log::warning('미리보기 PDF 생성도 실패, 원본 제공', ['error' => $e2->getMessage()]);
                    $filePath = $contract->original_file_path;
                }
            }
        } else {
            // 서명 전: 텍스트/날짜/체크박스 필드가 합성된 미리보기 PDF 생성
            try {
                $pdfService = new PdfSignatureService;
                $filePath = $pdfService->generatePreview($contract);
            } catch (\Throwable $e) {
                Log::warning('미리보기 PDF 생성 실패, 원본 제공', ['error' => $e->getMessage()]);
                $filePath = $contract->original_file_path;
            }
        }

        if (! Storage::disk('local')->exists($filePath)) {
            return response()->json(['success' => false, 'message' => '문서 파일이 존재하지 않습니다.'], 404);
        }

        $fileName = $contract->original_file_name ?: ($contract->title.'.pdf');

        return Storage::disk('local')->download($filePath, $fileName, [
            'Content-Type' => 'application/pdf',
        ]);
    }

    // ─── Private ───

    /**
     * SMS로 OTP 발송 (바로빌 독립 SMS API 사용)
     */
    private function sendOtpViaSms(EsignContract $contract, EsignSigner $signer, string $otpCode): bool
    {
        try {
            $member = BarobillMember::where('tenant_id', $contract->tenant_id)->first();
            if (! $member || ! $member->manager_hp) {
                Log::warning('OTP SMS 발송 실패: 바로빌 회원 또는 발신번호 없음', [
                    'contract_id' => $contract->id,
                    'tenant_id' => $contract->tenant_id,
                ]);

                return false;
            }

            $barobill = app(BarobillService::class);
            $barobill->setServerMode($member->server_mode ?? 'production');

            $fromNumber = preg_replace('/[^0-9]/', '', $member->manager_hp);
            $toNumber = preg_replace('/[^0-9]/', '', $signer->phone);
            $smsText = "[SAM] 전자계약 인증코드: {$otpCode} (5분 이내 입력)";

            $result = $barobill->sendSMSMessage(
                corpNum: $member->biz_no,
                senderId: $member->barobill_id,
                fromNumber: $fromNumber,
                toName: $signer->name,
                toNumber: $toNumber,
                contents: $smsText,
            );

            if ($result['success'] ?? false) {
                return true;
            }

            Log::warning('OTP SMS 발송 API 실패', [
                'contract_id' => $contract->id,
                'signer_id' => $signer->id,
                'error' => $result['error'] ?? 'Unknown',
            ]);

            return false;
        } catch (\Throwable $e) {
            Log::error('OTP SMS 발송 예외', [
                'contract_id' => $contract->id,
                'signer_id' => $signer->id,
                'error' => $e->getMessage(),
            ]);

            return false;
        }
    }

    private function sendCompletionAlimtalk(EsignContract $contract, EsignSigner $signer): array
    {
        try {
            $member = BarobillMember::where('tenant_id', $contract->tenant_id)->first();
            if (! $member || ! $member->biz_no) {
                return ['success' => false, 'channel' => 'alimtalk', 'error' => '바로빌 회원 미등록'];
            }

            $barobill = app(BarobillService::class);
            $barobill->setServerMode($member->server_mode ?? 'production');

            // 채널 ID 조회
            $channelId = $this->getKakaotalkChannelId($barobill, $member->biz_no);
            if (! $channelId) {
                return ['success' => false, 'channel' => 'alimtalk', 'error' => '등록된 카카오톡 채널이 없습니다'];
            }

            $templateName = $this->resolveTemplateName('전자계약_완료');
            $documentUrl = config('app.url').'/esign/sign/'.$signer->access_token.'/api/document';
            $signUrl = config('app.url').'/esign/sign/'.$signer->access_token;
            $completedAt = $contract->completed_at?->format('Y-m-d H:i') ?? now()->format('Y-m-d H:i');

            // 등록된 템플릿 본문 + 버튼 조회
            $tplData = $this->getTemplateData($barobill, $member->biz_no, $channelId, $templateName);
            $templateContent = $tplData['content'];
            $templateButtons = $tplData['buttons'];

            if ($templateContent) {
                $message = str_replace(
                    ['#{이름}', '#{계약명}', '#{완료일}'],
                    [$signer->name, $contract->title, $completedAt],
                    $templateContent
                );
            } else {
                Log::warning('E-Sign 완료 알림톡: 템플릿 내용 조회 실패, 하드코딩 폴백 사용', [
                    'template_name' => $templateName,
                    'channel_id' => $channelId,
                ]);
                $message = "안녕하세요, {$signer->name}님.\n전자계약이 모든 서명자의 서명 완료로 확정되었습니다.\n\n■ 계약명: {$contract->title}\n■ 완료일: {$completedAt}\n\n아래 버튼에서 서명 완료된 계약서를 확인하고 다운로드할 수 있습니다.";
            }

            // 버튼: 템플릿에서 가져온 URL의 #{토큰}만 치환
            $buttons = ! empty($templateButtons) ? $templateButtons : [
                [
                    'Name' => '계약서 다운로드',
                    'ButtonType' => 'WL',
                    'Url1' => $documentUrl,
                    'Url2' => $documentUrl,
                ],
            ];

            foreach ($buttons as &$btn) {
                foreach (['Url1', 'Url2'] as $urlKey) {
                    if (! empty($btn[$urlKey])) {
                        $btn[$urlKey] = str_replace(
                            ['#{토큰}', '#{%ED%86%A0%ED%81%B0}'],
                            [$signer->access_token, $signer->access_token],
                            urldecode($btn[$urlKey])
                        );
                        // 완료 알림톡: 버튼 URL을 문서 다운로드 엔드포인트로 강제 변경
                        // 템플릿 버튼 URL이 서명 페이지(/esign/sign/{token})를 가리키므로
                        // 완료된 계약서 PDF 다운로드(/esign/sign/{token}/api/document)로 교체
                        if (str_contains($btn[$urlKey], '/esign/sign/') && ! str_contains($btn[$urlKey], '/api/document')) {
                            $btn[$urlKey] = $documentUrl;
                        }
                    }
                }
            }
            unset($btn);

            $receiverNum = preg_replace('/[^0-9]/', '', $signer->phone);

            Log::info('E-Sign 완료 알림톡 발송 시도', [
                'contract_id' => $contract->id,
                'signer_id' => $signer->id,
                'signer_role' => $signer->role,
                'template_name' => $templateName,
                'template_from_api' => (bool) $templateContent,
                'buttons_from_api' => ! empty($templateButtons),
                'receiver_num' => $receiverNum,
            ]);

            $result = $barobill->sendATKakaotalkEx(
                corpNum: $member->biz_no,
                senderId: $member->barobill_id,
                yellowId: $channelId,
                templateName: $templateName,
                receiverName: $signer->name,
                receiverNum: $receiverNum,
                title: '',
                message: $message,
                buttons: $buttons,
                smsMessage: ($contract->sms_fallback ?? true)
                    ? "[SAM] {$signer->name}님, 전자계약이 완료되었습니다. 계약서 다운로드: {$documentUrl}"
                    : '',
            );

            // 발송 접수 후 결과 확인
            if (($result['success'] ?? false) && ! empty($result['data']) && is_string($result['data'])) {
                $sendKey = $result['data'];
                Log::info('E-Sign 완료 알림톡 접수 성공', [
                    'contract_id' => $contract->id,
                    'send_key' => $sendKey,
                ]);

                sleep(3);
                $sendResult = $barobill->getSendKakaotalk($member->biz_no, $sendKey);
                $resultData = $sendResult['data'] ?? null;
                $resultCode = is_object($resultData) ? ($resultData->ResultCode ?? null) : ($resultData['ResultCode'] ?? null);
                $resultMsg = is_object($resultData) ? ($resultData->ResultMessage ?? null) : ($resultData['ResultMessage'] ?? null);

                Log::info('E-Sign 완료 알림톡 전달 결과', [
                    'contract_id' => $contract->id,
                    'send_key' => $sendKey,
                    'result_code' => $resultCode,
                    'result_message' => $resultMsg,
                ]);

                if ($resultCode !== null && $resultCode != 1) {
                    return [
                        'success' => false,
                        'channel' => 'alimtalk',
                        'error' => "카카오톡 전달 실패: {$resultMsg} (코드: {$resultCode})",
                    ];
                }
            }

            if (! ($result['success'] ?? false)) {
                Log::warning('E-Sign 완료 알림톡 발송 실패', [
                    'contract_id' => $contract->id,
                    'signer_id' => $signer->id,
                    'error' => $result['error'] ?? 'Unknown error',
                ]);

                return ['success' => false, 'channel' => 'alimtalk', 'error' => $result['error'] ?? 'API 호출 실패'];
            }

            return ['success' => true, 'channel' => 'alimtalk', 'error' => null];
        } catch (\Throwable $e) {
            Log::warning('E-Sign 완료 알림톡 발송 실패', [
                'contract_id' => $contract->id,
                'signer_id' => $signer->id,
                'error' => $e->getMessage(),
            ]);

            return ['success' => false, 'channel' => 'alimtalk', 'error' => $e->getMessage()];
        }
    }

    /**
     * 카카오톡 채널 ID 조회 (바로빌 API)
     */
    private function getKakaotalkChannelId(BarobillService $barobill, string $bizNo): ?string
    {
        $result = $barobill->getKakaotalkChannels($bizNo);

        if (! ($result['success'] ?? false) || empty($result['data'])) {
            return null;
        }

        $data = $result['data'];

        if (is_object($data) && isset($data->KakaotalkChannel)) {
            $channels = is_array($data->KakaotalkChannel)
                ? $data->KakaotalkChannel
                : [$data->KakaotalkChannel];
        } elseif (is_array($data) && isset($data['KakaotalkChannel'])) {
            $channels = is_array($data['KakaotalkChannel'])
                ? $data['KakaotalkChannel']
                : [$data['KakaotalkChannel']];
        } else {
            $channels = is_array($data) ? $data : [$data];
        }

        $channel = $channels[0] ?? null;

        if (! $channel) {
            return null;
        }

        return is_array($channel)
            ? ($channel['ChannelId'] ?? null)
            : ($channel->ChannelId ?? null);
    }

    /**
     * 바로빌 등록 템플릿의 본문 내용 및 버튼 정보 조회
     *
     * @return array{content: string|null, buttons: array}
     */
    private function getTemplateData(BarobillService $barobill, string $bizNo, string $channelId, string $templateName): array
    {
        $empty = ['content' => null, 'buttons' => []];

        $result = $barobill->getKakaotalkTemplates($bizNo, $channelId);

        if (! ($result['success'] ?? false) || empty($result['data'])) {
            return $empty;
        }

        $data = $result['data'];
        $items = [];

        if (is_object($data) && isset($data->KakaotalkTemplate)) {
            $items = is_array($data->KakaotalkTemplate)
                ? $data->KakaotalkTemplate
                : [$data->KakaotalkTemplate];
        }

        foreach ($items as $tpl) {
            if (($tpl->TemplateName ?? '') === $templateName) {
                $buttons = [];
                $btnData = $tpl->Buttons ?? null;
                if ($btnData) {
                    $btnList = $btnData->KakaotalkButton ?? null;
                    if ($btnList) {
                        $btnList = is_array($btnList) ? $btnList : [$btnList];
                        foreach ($btnList as $btn) {
                            $buttons[] = [
                                'Name' => $btn->Name ?? '',
                                'ButtonType' => $btn->ButtonType ?? 'WL',
                                'Url1' => $btn->Url1 ?? '',
                                'Url2' => $btn->Url2 ?? '',
                            ];
                        }
                    }
                }

                return [
                    'content' => $tpl->TemplateContent ?? null,
                    'buttons' => $buttons,
                ];
            }
        }

        return $empty;
    }

    /**
     * 환경별 알림톡 템플릿명 반환 (운영: 원본, 개발: _DEV 접미사)
     */
    private function resolveTemplateName(string $baseName): string
    {
        return $baseName.(app()->environment('production') ? '' : '_DEV');
    }

    private function findSigner(string $token): ?EsignSigner
    {
        $signer = EsignSigner::withoutGlobalScopes()
            ->where('access_token', $token)
            ->first();

        if ($signer && $signer->token_expires_at && $signer->token_expires_at->isPast()) {
            return null;
        }

        return $signer;
    }

    private function hasCompanyStamp(int $tenantId): bool
    {
        $stamp = TenantSetting::where('tenant_id', $tenantId)
            ->where('setting_group', 'esign')
            ->where('setting_key', 'company_stamp')
            ->first();

        if (! $stamp) {
            return false;
        }

        $val = $stamp->setting_value;

        return ! empty($val['gcs_object']) || ! empty($val['local_path']);
    }

    private function applyCompanyStamp(EsignSigner $signer, int $tenantId): ?string
    {
        $stamp = TenantSetting::where('tenant_id', $tenantId)
            ->where('setting_group', 'esign')
            ->where('setting_key', 'company_stamp')
            ->first();

        if (! $stamp) {
            return null;
        }

        $val = $stamp->setting_value;
        $imageData = null;

        if (! empty($val['gcs_object'])) {
            $gcs = app(\App\Services\GoogleCloudStorageService::class);
            $signedUrl = $gcs->getSignedUrl($val['gcs_object'], 5);
            if ($signedUrl) {
                $imageData = @file_get_contents($signedUrl);
            }
        } elseif (! empty($val['local_path']) && Storage::disk('local')->exists($val['local_path'])) {
            $imageData = Storage::disk('local')->get($val['local_path']);
        }

        if (! $imageData) {
            return null;
        }

        $localPath = "esign/{$tenantId}/signatures/{$signer->contract_id}_{$signer->id}_stamp.png";
        Storage::disk('local')->put($localPath, $imageData);
        $signer->update(['signature_image_path' => $localPath]);

        return $localPath;
    }
}
