<?php

namespace App\Services\Admin;

use App\Repositories\Admin\AdminUserRepository;
use App\Services\Admin\AdminAuditService;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;


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

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

        return [
            'id' => (int)($admin->id ?? 0),
            'email' => (string)($admin->email ?? ''),
            'name' => (string)($admin->name ?? ''),
            'nickname' => (string)($admin->nickname ?? ''),
            'status' => (string)($admin->status ?? ''),
            '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,
            'locked_until' => $admin->locked_until ? (string)$admin->locked_until : null,
            'failed_login_count' => (int)($admin->failed_login_count ?? 0),
            'must_reset_password' => (int)($admin->must_reset_password ?? 0),
        ];
    }

    private function packRoles(array $roles): array
    {
        // repo->getRolesForUser()가 반환하는 형태(id, code, name)를
        // 네가 원하는 id/name/label로 정리
        return array_map(function ($r) {
            $r = (array)$r;
            return [
                'id' => (int)($r['id'] ?? 0),
                'name' => (string)($r['code'] ?? ''),   // ex) super_admin
                'label' => (string)($r['name'] ?? ''),  // ex) 최고관리자
            ];
        }, $roles);
    }

    private function snapRolesForUser(int $adminId): array
    {
        $roles = $this->packRoles($this->repo->getRolesForUser($adminId));

        return [
            'roles' => $roles,
            'role_ids' => array_values(array_map(fn($x)=> (int)$x['id'], $roles)),
            'role_names' => array_values(array_map(fn($x)=> (string)$x['name'], $roles)),
        ];
    }

    public function list(array $filters): array
    {
        $page = $this->repo->paginateUsers($filters, 20);

        $ids = $page->getCollection()->pluck('id')->map(fn($v)=>(int)$v)->all();
        $roleMap = $this->repo->getRoleMapForUsers($ids);

        // permission count는 테이블 없을 수도 있으니 안전하게
        $permCnt = method_exists($this->repo, 'getPermissionCountMapForUsers')
            ? $this->repo->getPermissionCountMapForUsers($ids)
            : [];

        return [
            'page' => $page,
            'filters' => $filters,
            'roleMap' => $roleMap,
            'permCnt' => $permCnt,
        ];
    }

    public function editData(int $id): array
    {
        $admin = $this->repo->find($id);
        $roles = $admin ? $this->repo->getRolesForUser($id) : [];
        $allRoles = $this->repo->getAllRoles();
        $roleIds = $admin ? $this->repo->getRoleIdsForUser($id) : [];

        // phone 보여주기(암호화/시리얼라이즈 혼재 방어)
        $phone = null;
        if ($admin && property_exists($admin, 'phone_enc') && $admin->phone_enc) {
            $phone = $this->readPhone((string)$admin->phone_enc);
        }

        return [
            'admin' => $admin,
            'phone' => $phone,
            'roles' => $roles,
            'allRoles' => $allRoles,
            'roleIds' => $roleIds,
        ];
    }

    public function update(int $id, array $input, int $actorId, string $ip = '', string $ua = ''): array
    {
        $admin = $this->repo->find($id);
        if (!$admin) return $this->fail('관리자를 찾을 수 없습니다.');

        //  before snapshot (admin + roles)
        $before = array_merge(
            ['admin' => $this->snapAdmin($admin)],
            $this->snapRolesForUser($id),
        );

        // 본인 계정 비활성화 방지
        if ((int)$id === (int)$actorId && isset($input['status']) && $input['status'] !== 'active') {
            return $this->fail('본인 계정은 비활성화할 수 없습니다.');
        }

        //  버그 수정: $data가 아니라 $input 기준
        $wantTotp = (int)($input['totp_enabled'] ?? 0) === 1;

        //  TOTP를 켜려면 "등록 완료" 상태여야 함 (secret + verified_at)
        if ($wantTotp && (empty($admin->totp_secret_enc) || empty($admin->totp_verified_at))) {
            return $this->fail('Google OTP가 등록 완료 상태가 아니라 선택할 수 없습니다.');
        }

        $data = [
            'name' => trim((string)($input['name'] ?? '')),
            'nickname' => trim((string)($input['nickname'] ?? '')),
            'status' => (string)($input['status'] ?? 'active'),
            'totp_enabled' => $wantTotp ? 1 : 0,
            'updated_by' => $actorId ?: null,
        ];

        // phone 처리(컬럼 있는 경우만)
        $rawPhone = trim((string)($input['phone'] ?? ''));
        if ($rawPhone !== '') {
            $phone = $this->normalizeKrPhone($rawPhone);
            if ($phone === '') return $this->fail('휴대폰 번호 형식이 올바르지 않습니다.');

            $hashKey = (string)config('admin.phone_hash_key', env('ADMIN_PHONE_HASH_KEY', ''));
            if ($hashKey === '') return $this->fail('ADMIN_PHONE_HASH_KEY 가 설정되지 않았습니다.');

            $e164 = $this->toE164Kr($phone);

            // update는 e164로 해시하니, 여기서도 e164 기준 유지
            $hash = hash_hmac('sha256', $e164, $hashKey);

            if (method_exists($this->repo, 'existsPhoneHash') && $this->repo->existsPhoneHash($hash, $id)) {
                return $this->fail('이미 사용 중인 휴대폰 번호입니다.');
            }

            $data['phone_hash'] = $hash;
            $data['phone_enc'] = Crypt::encryptString($e164);

            if (Schema::hasColumn('admin_users', 'phone_last4')) {
                $data['phone_last4'] = substr(preg_replace('/\D+/', '', $phone), -4);
            }
        }

        // null만 제외 (0은 유지)
        $data = array_filter($data, fn($v)=>$v !== null);

        $ok = $this->repo->updateById($id, $data);

        // ⚠️ updateById가 "변경사항 없음"이면 0 리턴할 수도 있음
        // 지금은 0이면 실패 처리인데, 원하면 아래처럼 완화 가능:
        // if (!$ok && empty($data)) { $ok = true; }
        if (!$ok) return $this->fail('저장에 실패했습니다. 잠시 후 다시 시도해 주세요.');

        // roles sync
        $roleIds = $input['role_ids'] ?? [];
        if (is_array($roleIds)) {
            $roleIds = array_values(array_filter(array_map('intval', $roleIds), fn($v)=>$v>0));
            $this->repo->syncRoles($id, $roleIds, $actorId);
        }

        //  after snapshot
        $afterAdmin = $this->repo->find($id);
        $after = array_merge(
            ['admin' => $this->snapAdmin($afterAdmin)],
            $this->snapRolesForUser($id),
        );

        //  Audit log (actor=수정한 관리자, target=수정당한 관리자)
        $this->audit->log(
            actorAdminId: $actorId,
            action: 'admin_user_update',
            targetType: 'admin_users',
            targetId: $id,
            before: $before,
            after: $after,
            ip: $ip,
            ua: $ua,
        );

        return $this->ok('변경되었습니다.');
    }


    public function resetPasswordToEmail(int $id, int $actorId, string $ip = '', string $ua = ''): array
    {
        $admin = $this->repo->find($id);
        if (!$admin) return $this->fail('관리자를 찾을 수 없습니다.');

        $before = ['admin' => $this->snapAdmin($admin)];

        $email = (string)($admin->email ?? '');
        if ($email === '') return $this->fail('이메일이 비어 있어 초기화할 수 없습니다.');

        $payload = [
            'password' => Hash::make($email), // ❗비번 원문/해시는 로그에 남기지 않음
            'must_reset_password' => 1,
            'password_changed_at' => now(),
            'updated_by' => $actorId ?: null,
            'updated_at' => now(),
        ];

        $ok = $this->repo->updateById($id, $payload);
        if (!$ok) return $this->fail('비밀번호 초기화에 실패했습니다.');

        $afterAdmin = $this->repo->find($id);
        $after = ['admin' => $this->snapAdmin($afterAdmin)];

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

        return $this->ok('임시 비밀번호가 이메일로 초기화되었습니다. (로그인 후 OTP 진행)');
    }


    public function unlock(int $id, int $actorId, string $ip = '', string $ua = ''): array
    {
        $admin = $this->repo->find($id);
        if (!$admin) return $this->fail('관리자를 찾을 수 없습니다.');

        $before = ['admin' => $this->snapAdmin($admin)];

        $ok = $this->repo->updateById($id, [
            'locked_until' => null,
            'failed_login_count' => 0,
            'updated_by' => $actorId ?: null,
        ]);

        if (!$ok) return $this->fail('잠금 해제에 실패했습니다.');

        $afterAdmin = $this->repo->find($id);
        $after = ['admin' => $this->snapAdmin($afterAdmin)];

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

        return $this->ok('잠금이 해제되었습니다.');
    }


    private function ok(string $msg): array { return ['ok'=>true,'message'=>$msg]; }
    private function fail(string $msg): array { return ['ok'=>false,'message'=>$msg]; }

    private function normalizeKrPhone(string $raw): string
    {
        $n = preg_replace('/\D+/', '', $raw) ?? '';
        if ($n === '') return '';
        if (str_starts_with($n, '82')) $n = '0'.substr($n, 2);
        if (!preg_match('/^01[016789]\d{7,8}$/', $n)) return '';
        return $n;
    }

    private function toE164Kr(string $raw010): string
    {
        $n = preg_replace('/\D+/', '', $raw010) ?? '';
        if ($n === '') return '+82';
        if (str_starts_with($n, '0')) $n = substr($n, 1);
        return '+82'.$n;
    }

    private function readPhone(string $phoneEnc): ?string
    {
        // s:11:"010..."; 형태(구데이터) 방어: 그냥 숫자만 뽑아냄
        if (preg_match('/^s:\d+:"([^"]+)";$/', $phoneEnc, $m)) {
            $n = preg_replace('/\D+/', '', $m[1]) ?? '';
            return $n ?: null;
        }

        // decrypt 시도
        try {
            $v = Crypt::decryptString($phoneEnc);
            $n = preg_replace('/\D+/', '', $v) ?? '';
            if ($n === '') return null;
            if (str_starts_with($n, '82')) $n = '0'.substr($n, 2);
            return $n;
        } catch (\Throwable $e) {
            return null;
        }
    }

    public function getAssignableRoles(): array
    {
        // value(code) => label
        return [
            ['code' => 'super_admin', 'label' => '최고관리자'],
            ['code' => 'finance',     'label' => '정산관리'],
            ['code' => 'product',     'label' => '상품관리'],
            ['code' => 'support',     'label' => 'CS/상담'],
        ];
    }

    public function createAdmin(array $payload, int $actorId, string $ip = '', string $ua = ''): array
    {
        $email = (string)($payload['email'] ?? '');
        $name  = (string)($payload['name'] ?? '');
        $nick  = (string)($payload['nickname'] ?? '');
        $roleCode = (string)($payload['role'] ?? '');
        $phoneDigits = (string)($payload['phone_digits'] ?? '');

        $hashKey = (string) config('admin.phone_hash_key', env('ADMIN_PHONE_HASH_KEY', ''));
        if ($hashKey === '') {
            return $this->fail('설정 오류: ADMIN_PHONE_HASH_KEY 가 비어있어 등록할 수 없습니다.');
        }

        // role 존재 확인 (DB에 실제 role row가 있어야 함)
        $role = $this->repo->findRoleByCode($roleCode);
        if (!$role) {
            return $this->fail('역할(Role) 설정이 올바르지 않습니다. (role not found)');
        }
        $roleId = (int)($role['id'] ?? 0);
        if ($roleId < 1) {
            return $this->fail('역할(Role) 설정이 올바르지 않습니다.');
        }

        // phone_* 생성
        $phoneEnc   = Crypt::encryptString($phoneDigits);
        $phoneLast4 = substr($phoneDigits, -4);
        $phoneHash  = hash_hmac('sha256', $phoneDigits, $hashKey);

        // 임시 비밀번호(서버 생성)
        $tempPassword = $email;
        $pwHash = Hash::make($tempPassword); // 단방향 해시 (복호화 불가)

        try {
            return DB::transaction(function () use (
                $email, $name, $nick,
                $phoneEnc, $phoneHash, $phoneLast4,
                $pwHash, $tempPassword,
                $roleId, $actorId, $ip, $ua
            ) {
                // 중복 방어(이메일/휴대폰)
                if ($this->repo->existsByEmail($email)) {
                    return $this->fail('이미 등록된 이메일입니다.');
                }
                if ($this->repo->existsByPhoneHash($phoneHash)) {
                    return $this->fail('이미 등록된 휴대폰 번호입니다.');
                }

                //  admin_users insert
                $adminId = $this->repo->insertAdminUser([
                    'email' => $email,
                    'password' => $pwHash,
                    'name' => $name,
                    'nickname' => $nick,
                    'phone_enc' => $phoneEnc,
                    'phone_hash' => $phoneHash,
                    'phone_last4' => $phoneLast4,

                    'status' => 'active',
                    'must_reset_password' => 1,

                    // 신규는 기본 SMS 인증으로 시작(너 정책: 0=sms, 1=otp)
                    'totp_enabled' => 0,
                    'totp_secret_enc' => null,
                    'totp_verified_at' => null,

                    'failed_login_count' => 0,
                    'locked_until' => null,

                    'created_by' => $actorId ?: null,
                    'updated_by' => $actorId ?: null,
                    'created_at' => now(),
                    'updated_at' => now(),
                ]);

                if ($adminId < 1) {
                    return $this->fail('관리자 생성에 실패했습니다.');
                }

                // 단일 role 유지: 기존 role 매핑 제거 후 updateOrInsert
                $this->repo->deleteRoleMappingsExcept($adminId, $roleId);
                $this->repo->upsertRoleMapping($adminId, $roleId);

                $afterAdmin = $this->repo->find($adminId);
                $after = array_merge(
                    ['admin' => $this->snapAdmin($afterAdmin)],
                    $this->snapRolesForUser($adminId),
                );

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

                return [
                    'ok' => true,
                    'admin_id' => $adminId,
                    'temp_password' => '', // 노출 안함(어차피 이메일)
                    'message' => '관리자 계정이 등록되었습니다. 임시 비밀번호는 이메일입니다. (다음 로그인 시 변경 강제)',
                ];
            });
        } catch (\Throwable $e) {
            return $this->fail('등록 중 오류가 발생했습니다. (DB)');
        }
    }
}
