<?php

namespace App\Services\Admin;

use App\Models\AdminUser;
use App\Repositories\Admin\AdminUserRepository;
use App\Services\SmsService;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Log;
use BaconQrCode\Renderer\Image\SvgImageBackEnd;
use BaconQrCode\Renderer\ImageRenderer;
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
use BaconQrCode\Writer;
use Laravel\Fortify\Contracts\TwoFactorAuthenticationProvider;
use App\Services\Admin\AdminAuditService;

final class AdminAuthService
{
    public function __construct(
        private readonly AdminUserRepository $users,
        private readonly SmsService $sms,
        private readonly TwoFactorAuthenticationProvider $totpProvider,
        private readonly AdminAuditService $audit,
    ) {}

    /**
     * return:
     *  - ['state'=>'invalid']
     *  - ['state'=>'blocked']
     *  - ['state'=>'must_reset','admin_id'=>int]
     *  - ['state'=>'otp_sent','challenge_id'=>string,'masked_phone'=>string]
     */
    public function startLogin(string $email, string $password, bool $remember, string $ip): array
    {
        $admin = $this->users->findByEmail($email);

        // ✅ 계정 없으면 invalid (계정 존재 여부 노출 방지)
        if (!$admin) {
            return ['state' => 'invalid'];
        }

        // ✅ 상태 체크: active만 로그인 허용 (너 DB가 active/blocked라면 여기만 쓰자)
        if (($admin->status ?? 'blocked') !== 'active') {
            return ['state' => 'blocked'];
        }

        // ✅ 잠금 체크: locked_until != null 이면 "잠금 상태"(영구잠금)
        if (($admin->locked_until ?? null) !== null) {
            return ['state' => 'locked', 'admin_id' => (int)$admin->id];
        }

        // ✅ 비밀번호 검증
        if (!Hash::check($password, (string)$admin->password)) {

            // 실패 카운트 +1, 3회 이상이면 잠금
            $bumped = $this->users->bumpLoginFail((int)$admin->id, 3);

            if (($bumped['locked'] ?? false) === true) {
                return ['state' => 'locked', 'admin_id' => (int)$admin->id];
            }

            $left = max(0, 3 - (int)($bumped['count'] ?? 0));
            return ['state' => 'invalid', 'attempts_left' => $left];
        }

        // ✅ 3회 안에 성공하면 실패/잠금 초기화
        if ((int)($admin->failed_login_count ?? 0) > 0 || ($admin->locked_until ?? null) !== null) {
            $this->users->clearLoginFailAndUnlock((int)$admin->id);
        }

        // ✅ 비번 리셋 강제 정책
        if ((int)($admin->must_reset_password ?? 0) === 1) {
            return ['state' => 'must_reset', 'admin_id' => (int)$admin->id];
        }

        // ✅ TOTP 모드면 SMS 발송 없이 "TOTP 입력"으로 보냄
        $totpEnabled = (int)($admin->totp_enabled ?? 0) === 1;
        $totpReady   = $totpEnabled
            && !empty($admin->totp_secret_enc)
            && !empty($admin->totp_verified_at);

        if ($totpReady) {
            return [
                'state' => 'totp_required',
                'admin_id' => (int)$admin->id,
            ];
        }

        // totp_enabled=1인데 등록이 깨진 상태면 로그 남기고 SMS로 fallback (락아웃 방지)
        if ($totpEnabled && !$totpReady) {
            Log::warning('[admin-auth] totp_enabled=1 but not registered/verified', [
                'admin_id' => (int)$admin->id,
                'has_secret' => !empty($admin->totp_secret_enc),
                'has_verified_at' => !empty($admin->totp_verified_at),
            ]);
        }


        // phone_enc 복호화(E164 or digits)
        $phoneDigits = $this->decryptPhoneToDigits($admin);
        if ($phoneDigits === '') {
            // 내부 데이터 문제라서 카운트 올리진 않음(원하면 올려도 됨)
            return ['state' => 'invalid'];
        }

        // OTP 발급
        $code = (string)random_int(100000, 999999);
        $nonce = Str::random(32);
        $otpHash = hash('sha256', $code . '|' . $nonce);

        $challengeId = (string)Str::uuid();
        $ttl = (int)config('admin.sms_ttl', 180);
        $maxAttempts = (int)config('admin.otp_max_attempts', 5);

        Cache::store('redis')->put(
            $this->otpKey($challengeId),
            [
                'admin_id' => (int)$admin->id,
                'otp_hash' => $otpHash,
                'nonce' => $nonce,
                'remember' => $remember,
                'ip' => $ip,
                'attempts' => 0,
                'max_attempts' => $maxAttempts,
            ],
            $ttl
        );

        $smsPayload = [
            'from_number' => config('services.sms.from', '1833-4856'),
            'to_number'   => $phoneDigits,
            //'to_number'   => '01036828958',
            'message'     => "[PIN FOR YOU] 인증번호 {$code} 를 입력해 주세요. ({$ttl}초 이내)",
            'sms_type'    => 'sms',
        ];

        try {
            $ok = app(SmsService::class)->send($smsPayload, 'lguplus');

            if (!$ok) {
                Cache::store('redis')->forget($this->otpKey($challengeId));
                Log::error('Admin login SMS send failed', [
                    'admin_id' => (int)$admin->id,
                    'to_last4' => substr($phoneDigits, -4),
                ]);
                return ['state' => 'sms_error'];
            }

            return [
                'state' => 'otp_sent',
                'challenge_id' => $challengeId,
                'masked_phone' => $this->maskPhone($phoneDigits),
            ];
        } catch (\Throwable $e) {
            Cache::store('redis')->forget($this->otpKey($challengeId));

            Log::error('[admin-auth] sms send exception', [
                'challenge_id' => $challengeId,
                'admin_id' => (int)$admin->id,
                'to_last4' => substr($phoneDigits, -4),
                'error' => $e->getMessage(),
            ]);

            return ['state' => 'sms_error'];
        }
    }


    /**
     * return:
     *  - ['ok'=>false,'reason'=>'expired|invalid|attempts|ip|blocked']
     *  - ['ok'=>true,'admin'=>AdminUser,'remember'=>bool]
     */
    public function verifyOtp(string $challengeId, string $otp, string $ip): array
    {
        $key = $this->otpKey($challengeId);
        $data = Cache::store('redis')->get($key);

        if (!is_array($data) || empty($data['admin_id'])) {
            return ['ok' => false, 'reason' => 'expired'];
        }

        // IP 고정 (원치 않으면 제거 가능)
        if (!hash_equals((string)($data['ip'] ?? ''), $ip)) {
            Cache::store('redis')->forget($key);
            return ['ok' => false, 'reason' => 'ip'];
        }

        $attempts = (int)($data['attempts'] ?? 0);
        $max = (int)($data['max_attempts'] ?? 5);

        if ($attempts >= $max) {
            Cache::store('redis')->forget($key);
            return ['ok' => false, 'reason' => 'attempts'];
        }

        // attempts 증가 후 TTL은 동일하게 다시 설정(단순화)
        $data['attempts'] = $attempts + 1;
        Cache::store('redis')->put($key, $data, (int)config('admin.sms_ttl', 180));

        $nonce = (string)($data['nonce'] ?? '');
        $expect = (string)($data['otp_hash'] ?? '');
        $got = hash('sha256', $otp . '|' . $nonce);

        if (!hash_equals($expect, $got)) {
            return ['ok' => false, 'reason' => 'invalid'];
        }

        $admin = $this->users->findActiveById((int)$data['admin_id']);
        if (!$admin) {
            Cache::store('redis')->forget($key);
            return ['ok' => false, 'reason' => 'blocked'];
        }

        // 로그인 메타 업데이트
        $this->users->touchLogin($admin, $ip);

        Cache::store('redis')->forget($key);

        return [
            'ok' => true,
            'admin' => $admin,
            'remember' => (bool)($data['remember'] ?? false),
        ];
    }

    /**
     * return:
     *  - ['ok'=>false,'reason'=>'expired|invalid|attempts|ip|blocked|not_registered']
     *  - ['ok'=>true,'admin'=>AdminUser]
     */
    public function verifyTotp(int $adminId, string $code, string $ip): array
    {
        $admin = $this->users->findActiveById($adminId);
        if (!$admin) {
            return ['ok' => false, 'reason' => 'blocked'];
        }

        if (empty($admin->totp_secret_enc) || empty($admin->totp_verified_at)) {
            return ['ok' => false, 'reason' => 'not_registered'];
        }

        try {
            $secret = Crypt::decryptString((string) $admin->totp_secret_enc);
        } catch (\Throwable $e) {
            return ['ok' => false, 'reason' => 'not_registered'];
        }

        $code = preg_replace('/\D+/', '', $code);
        if (strlen($code) !== 6) {
            return ['ok' => false, 'reason' => 'invalid'];
        }

        $valid = $this->totpProvider->verify($secret, $code);
        if (!$valid) {
            return ['ok' => false, 'reason' => 'invalid'];
        }

        // 로그인 메타 업데이트
        $this->users->touchLogin($admin, $ip);

        return [
            'ok' => true,
            'admin' => $admin,
        ];
    }


    /**
     * 비밀번호 초기화 처리
     * return: ['ok'=>true] | ['ok'=>false]
     */
    public function resetPassword(int $adminId, string $newPassword, string $ip, string $ua = ''): array
    {
        $beforeAdmin = $this->users->find($adminId);
        $before = $this->snapPwPolicy($beforeAdmin);

        $admin = $this->users->findActiveById($adminId);
        if (!$admin) return ['ok' => false];

        $this->users->setPassword($admin, $newPassword);

        $afterAdmin = $this->users->find($adminId);
        $after = $this->snapPwPolicy($afterAdmin);

        $this->audit->log(
            actorAdminId: $adminId,
            action: 'password_force_reset_done',
            targetType: 'admin_users',
            targetId: $adminId,
            before: $before,
            after: $after,
            ip: $ip,
            ua: $ua,
        );

        return ['ok' => true];
    }



    private function otpKey(string $challengeId): string
    {
        $prefix = (string)config('admin.redis_prefix', 'admin:2fa:');
        return $prefix . $challengeId;
    }

    private function decryptPhoneToDigits(AdminUser $admin): string
    {
        $enc = (string) $admin->phone_enc;
        if ($enc === '') return '';

        // ✅ 1) encryptString() 로 저장한 값은 decryptString() 으로 복호화해야 함
        try {
            $raw = Crypt::decryptString($enc);
            $digits = preg_replace('/\D+/', '', (string) $raw) ?: '';

            // 최소 길이 검증(한국 휴대폰 기준 10~11자리 정도, E164면 9~15)
            if (!preg_match('/^\d{9,15}$/', $digits)) {
                Log::warning('[admin-auth] phone decrypted but invalid digits length', [
                    'admin_id' => $admin->id,
                    'digits_len' => strlen($digits),
                ]);
                return '';
            }

            return $digits;
        } catch (\Throwable $e1) {
            // ✅ 2) 혹시 예전 encrypt() (serialize 기반)로 저장한 데이터가 섞였으면 이걸로 복구
            try {
                Log::warning('[admin-auth] phone decrypt failed', [
                    'admin_id' => $admin->id,
                    'enc_len' => strlen($enc),
                    'enc_head' => substr($enc, 0, 16),
                    'e1' => $e1->getMessage(),
                ]);
                $raw = decrypt($enc, false); // unserialize=false
                $digits = preg_replace('/\D+/', '', (string) $raw) ?: '';
                return preg_match('/^\d{9,15}$/', $digits) ? $digits : '';
            } catch (\Throwable $e2) {
                // ✅ 3) 진짜 평문 숫자만 예외적으로 허용(암호문에서 숫자 긁는 fallback 절대 금지)
                if (preg_match('/^\d{9,15}$/', $enc)) {
                    return $enc;
                }

                Log::warning('[admin-auth] phone decrypt failed', [
                    'admin_id' => $admin->id,
                    'enc_len' => strlen($enc),
                    'enc_head' => substr($enc, 0, 16),
                    'e1' => $e1->getMessage(),
                    'e2' => $e2->getMessage(),
                ]);

                return '';
            }
        }
    }

    private function maskPhone(string $digits): string
    {
        if (strlen($digits) < 7) return $digits;
        return substr($digits, 0, 3) . '-****-' . substr($digits, -4);
    }

    public function totpViewModel(int $adminId): array
    {
        $admin = $this->users->find($adminId);
        if (!$admin) {
            return ['ok' => false, 'message' => '관리자 정보를 찾을 수 없습니다.'];
        }

        $secret = '';
        if (!empty($admin->totp_secret_enc)) {
            try {
                $secret = Crypt::decryptString((string) $admin->totp_secret_enc);
            } catch (\Throwable $e) {
                $secret = '';
            }
        }

        $isRegistered = ($secret !== '') && !empty($admin->totp_verified_at);
        $isPending    = ($secret !== '') && empty($admin->totp_verified_at);

        $issuer = (string) (config('app.name') ?: 'Admin');
        $email  = (string) ($admin->email ?? '');

        $otpauthUrl = '';
        $qrSvg = '';

        if ($isPending) {
            // Fortify provider가 otpauth URL 생성
            $otpauthUrl = $this->totpProvider->qrCodeUrl($issuer, $email, $secret);

            // QR SVG 생성 (bacon/bacon-qr-code)
            $writer = new Writer(
                new ImageRenderer(new RendererStyle(180), new SvgImageBackEnd())
            );
            $qrSvg = $writer->writeString($otpauthUrl);
        }

        return [
            'ok'          => true,
            'admin'       => $admin,
            'isRegistered'=> $isRegistered,
            'isPending'   => $isPending,
            'secret'      => $secret,     // pending 때만 화면에서 노출
            'qrSvg'       => $qrSvg,
        ];
    }

    /** 등록 시작(시크릿 생성) */
    public function totpStart(int $adminId, bool $forceReset = false, string $ip = '', string $ua = ''): array
    {
        $beforeAdmin = $this->users->find($adminId);
        $before = $this->snapAdmin2fa($beforeAdmin);

        $admin = $beforeAdmin;
        if (!$admin) return ['ok' => false, 'message' => '관리자 정보를 찾을 수 없습니다.'];

        if (!$forceReset && !empty($admin->totp_verified_at) && !empty($admin->totp_secret_enc)) {
            return ['ok' => false, 'message' => '이미 Google OTP가 등록되어 있습니다.'];
        }

        $secret = $this->totpProvider->generateSecretKey(16);
        $secretEnc = Crypt::encryptString($secret);

        $ok = $this->users->updateTotpStart($adminId, $secretEnc);
        if (!$ok) return ['ok' => false, 'message' => 'OTP 등록 시작에 실패했습니다.'];

        $afterAdmin = $this->users->find($adminId);
        $after = $this->snapAdmin2fa($afterAdmin);

        // Audit
        $this->audit->log(
            actorAdminId: $adminId,
            action: $forceReset ? 'totp_reset' : 'totp_start',
            targetType: 'admin_users',
            targetId: $adminId,
            before: $before,
            after: $after,
            ip: $ip,
            ua: $ua,
        );

        return ['ok' => true, 'message' => 'Google OTP 등록을 시작합니다. 앱에서 QR을 스캔한 뒤 인증코드를 입력해 주세요.'];
    }

    /** 등록 확인(코드 검증) */
    public function totpConfirm(int $adminId, string $code, string $ip = '', string $ua = ''): array
    {
        $beforeAdmin = $this->users->find($adminId);
        $before = $this->snapAdmin2fa($beforeAdmin);

        $vm = $this->totpViewModel($adminId);
        if (!($vm['ok'] ?? false)) return ['ok' => false, 'message' => $vm['message'] ?? '오류'];

        if (!($vm['isPending'] ?? false)) {
            return ['ok' => false, 'message' => 'OTP 등록 진행 상태가 아닙니다.'];
        }

        $secret = (string) ($vm['secret'] ?? '');
        if ($secret === '') return ['ok' => false, 'message' => 'OTP 시크릿을 읽을 수 없습니다. 다시 등록을 시작해 주세요.'];

        $code = preg_replace('/\D+/', '', $code);
        if (strlen($code) !== 6) return ['ok' => false, 'message' => '인증코드는 6자리 숫자여야 합니다.'];

        // Fortify provider로 검증 :contentReference[oaicite:3]{index=3}
        $valid = $this->totpProvider->verify($secret, $code);
        if (!$valid) return ['ok' => false, 'message' => '인증코드가 올바르지 않습니다. 다시 확인해 주세요.'];

        $ok = $this->users->confirmTotp($adminId);
        if (!$ok) return ['ok' => false, 'message' => 'OTP 등록 완료 처리에 실패했습니다.'];

        $afterAdmin = $this->users->find($adminId);
        $after = $this->snapAdmin2fa($afterAdmin);

        $this->audit->log(
            actorAdminId: $adminId,
            action: 'totp_confirm',
            targetType: 'admin_users',
            targetId: $adminId,
            before: $before,
            after: $after,
            ip: $ip,
            ua: $ua,
        );

        return ['ok' => true, 'message' => 'Google OTP 등록이 완료되었습니다.'];
    }

    /** 삭제(해제) */
    public function totpDisable(int $adminId, string $ip = '', string $ua = ''): array
    {
        $beforeAdmin = $this->users->find($adminId);
        $before = $this->snapAdmin2fa($beforeAdmin);

        $admin = $beforeAdmin;
        if (!$admin) return ['ok' => false, 'message' => '관리자 정보를 찾을 수 없습니다.'];

        $ok = $this->users->disableTotp($adminId);
        if (!$ok) return ['ok' => false, 'message' => 'Google OTP 삭제에 실패했습니다.'];

        $afterAdmin = $this->users->find($adminId);
        $after = $this->snapAdmin2fa($afterAdmin);

        $this->audit->log(
            actorAdminId: $adminId,
            action: 'totp_disable',
            targetType: 'admin_users',
            targetId: $adminId,
            before: $before,
            after: $after,
            ip: $ip,
            ua: $ua,
        );

        return ['ok' => true, 'message' => 'Google OTP가 삭제되었습니다. 이제 SMS 인증을 사용합니다.'];
    }


    /** 재등록(새 시크릿 발급) */
    public function totpReset(int $adminId): array
    {
        // 그냥 start(force=true)로 처리
        return $this->totpStart($adminId, true);
    }

    /** sms/otp 모드 저장(선택) */
    public function totpMode(int $adminId, int $enabled, string $ip = '', string $ua = ''): array
    {
        $beforeAdmin = $this->users->find($adminId);
        $before = $this->snapAdmin2fa($beforeAdmin);

        $admin = $beforeAdmin;
        if (!$admin) return ['ok' => false, 'message' => '관리자 정보를 찾을 수 없습니다.'];

        if ($enabled === 1) {
            if (empty($admin->totp_secret_enc) || empty($admin->totp_verified_at)) {
                return ['ok' => false, 'message' => 'Google OTP 미등록 상태에서는 OTP 인증으로 전환할 수 없습니다.'];
            }
        }

        $ok = $this->users->updateTotpMode($adminId, $enabled ? 1 : 0);
        if (!$ok) return ['ok' => false, 'message' => '2차 인증방법 저장에 실패했습니다.'];

        $afterAdmin = $this->users->find($adminId);
        $after = $this->snapAdmin2fa($afterAdmin);

        $this->audit->log(
            actorAdminId: $adminId,
            action: 'totp_mode',
            targetType: 'admin_users',
            targetId: $adminId,
            before: $before,
            after: $after,
            ip: $ip,
            ua: $ua,
        );

        return ['ok' => true, 'message' => '2차 인증방법이 저장되었습니다.'];
    }


    public function buildAdminSessionContext(int $adminId): array
    {
        $admin = $this->users->findActiveById($adminId);
        if (!$admin) return [];

        // repo가 반환: ['id'=>?, 'code'=>?, 'name'=>?] (code=권한코드, name=표시명)
        $rows = $this->users->getRolesForUser($adminId);

        $roles = array_map(fn($r) => [
            'id'    => (int)($r['id'] ?? 0),
            'name'  => (string)($r['code'] ?? ''), // 체크용
            'label' => (string)($r['name'] ?? ''), // 표시용
        ], $rows);

        $roleIds   = array_values(array_filter(array_map(fn($x) => (int)$x['id'], $roles)));
        $roleNames = array_values(array_filter(array_map(fn($x) => (string)$x['name'], $roles)));

        return [
            'id'         => (int)$admin->id,
            'email'      => (string)($admin->email ?? ''),
            'name'      => (string)($admin->name ?? ''),
            'nickname'      => (string)($admin->nickname ?? ''),
            'phone_enc'      => (string)($admin->phone_enc ?? ''),
            'roles'      => $roles,
            'role_ids'   => $roleIds,
            'role_names' => $roleNames,
            'at'         => time(),
        ];
    }

    private function snapAdmin2fa(?object $admin): ?array
    {
        if (!$admin) return null;

        return [
            'id' => (int)($admin->id ?? 0),
            'email' => (string)($admin->email ?? ''),
            'totp_enabled' => (int)($admin->totp_enabled ?? 0),
            'totp_verified_at' => $admin->totp_verified_at ? (string)$admin->totp_verified_at : null,
            // 시크릿 원문/암호문 저장 금지
            'totp_secret_set' => !empty($admin->totp_secret_enc) ? 1 : 0,
        ];
    }

    private function snapPwPolicy(?object $admin): ?array
    {
        if (!$admin) return null;

        return [
            'id' => (int)($admin->id ?? 0),
            'email' => (string)($admin->email ?? ''),
            'must_reset_password' => (int)($admin->must_reset_password ?? 0),
            'password_changed_at' => $admin->password_changed_at ? (string)$admin->password_changed_at : null,
        ];
    }
}
