<?php

namespace App\Services;

use App\Mail\TwoFactorCodeMail;
use App\Models\CompanySetting;
use App\Models\User;
use App\Support\Totp;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;

class TwoFactorService
{
    public const METHOD_EMAIL = 'email';

    public const METHOD_TOTP = 'totp';

    public const POLICY_OFF = 'off';

    public const POLICY_OPTIONAL = 'optional';

    public const POLICY_REQUIRED = 'required';

    public const SETTING_POLICY = 'security.two_factor_policy';

    public const SETTING_DEFAULT_METHOD = 'security.two_factor_default_method';

    public function isEnabled(User $user): bool
    {
        return (bool) $user->two_factor_enabled;
    }

    public function method(User $user): string
    {
        return $user->two_factor_method ?: self::METHOD_EMAIL;
    }

    public function companyPolicy(User $user): string
    {
        $policy = $this->companySettingValue($user, self::SETTING_POLICY, self::POLICY_OPTIONAL);

        return in_array($policy, [self::POLICY_OFF, self::POLICY_OPTIONAL, self::POLICY_REQUIRED], true)
            ? $policy
            : self::POLICY_OPTIONAL;
    }

    public function companyDefaultMethod(User $user): string
    {
        $method = $this->companySettingValue($user, self::SETTING_DEFAULT_METHOD, self::METHOD_EMAIL);

        return in_array($method, [self::METHOD_EMAIL, self::METHOD_TOTP], true)
            ? $method
            : self::METHOD_EMAIL;
    }

    public function isRequiredByCompany(User $user): bool
    {
        return $this->companyPolicy($user) === self::POLICY_REQUIRED;
    }

    /** Company allows employees to use / manage 2FA in profile (optional or required). */
    public function isOfferedByCompany(User $user): bool
    {
        return $this->companyPolicy($user) !== self::POLICY_OFF;
    }

    /** User must complete 2FA setup (company mandate, not yet enrolled). */
    public function mustEnroll(User $user): bool
    {
        return $this->isRequiredByCompany($user) && ! $this->isEnabled($user);
    }

    /** Password verified — challenge OTP before session is created. */
    public function requiresChallenge(User $user): bool
    {
        if ($this->companyPolicy($user) === self::POLICY_OFF) {
            return $this->isEnabled($user);
        }

        return $this->isEnabled($user) || $this->isRequiredByCompany($user);
    }

    public function canDisable(User $user): bool
    {
        return ! $this->isRequiredByCompany($user);
    }

    /**
     * After password check: auto-enroll email when required, or send code.
     *
     * @return 'challenge'|'enroll'
     */
    public function beginLoginChallenge(User $user): string
    {
        if ($this->mustEnroll($user)) {
            if ($this->companyDefaultMethod($user) === self::METHOD_EMAIL) {
                $this->enableEmail($user);
                $this->sendLoginCode($user);

                return 'challenge';
            }

            return 'enroll';
        }

        if ($this->method($user) === self::METHOD_EMAIL) {
            $this->sendLoginCode($user);
        }

        return 'challenge';
    }

    protected function companySettingValue(User $user, string $key, string $default): string
    {
        if (! $user->company_id) {
            return $default;
        }

        $setting = CompanySetting::withoutGlobalScopes()
            ->where('company_id', $user->company_id)
            ->where('key', $key)
            ->first();

        $value = trim((string) ($setting?->value ?? ''));

        return $value !== '' ? $value : $default;
    }

    public function enableEmail(User $user): void
    {
        $user->forceFill([
            'two_factor_enabled' => true,
            'two_factor_method' => self::METHOD_EMAIL,
            'two_factor_secret' => null,
        ])->save();
    }

    public function enableTotp(User $user): string
    {
        $secret = Totp::generateSecret();
        $user->forceFill([
            'two_factor_enabled' => true,
            'two_factor_method' => self::METHOD_TOTP,
            'two_factor_secret' => Crypt::encryptString($secret),
        ])->save();

        return $secret;
    }

    /** @deprecated Use enableEmail() or enableTotp() */
    public function enable(User $user): string
    {
        $this->enableEmail($user);

        return Str::upper(Str::random(16));
    }

    public function disable(User $user): void
    {
        $user->forceFill([
            'two_factor_enabled' => false,
            'two_factor_method' => self::METHOD_EMAIL,
            'two_factor_secret' => null,
        ])->save();
    }

    /** Clear 2FA for all users in a company (e.g. when admin sets policy to Off). */
    public function disableForCompany(int $companyId): void
    {
        User::query()
            ->where('company_id', $companyId)
            ->where('two_factor_enabled', true)
            ->each(fn (User $user) => $this->disable($user));
    }

    public function totpSecret(User $user): ?string
    {
        if (! $user->two_factor_secret) {
            return null;
        }

        try {
            return Crypt::decryptString($user->two_factor_secret);
        } catch (\Throwable) {
            return null;
        }
    }

    public function totpProvisioningUri(User $user): ?string
    {
        $secret = $this->totpSecret($user);
        if (! $secret) {
            return null;
        }

        return Totp::provisioningUri($secret, $user->email ?: $user->name, config('app.name', 'KiyoHR'));
    }

    public function sendLoginCode(User $user): void
    {
        if ($this->method($user) === self::METHOD_TOTP) {
            return;
        }

        $code = (string) random_int(100000, 999999);
        Cache::put($this->cacheKey($user), $code, now()->addMinutes(10));

        if ($user->email) {
            Mail::to($user->email)->send(new TwoFactorCodeMail($code));
        }
    }

    public function verifyLoginCode(User $user, string $code): bool
    {
        if ($this->method($user) === self::METHOD_TOTP) {
            $secret = $this->totpSecret($user);

            return $secret && Totp::verify($secret, $code);
        }

        $expected = Cache::get($this->cacheKey($user));

        return is_string($expected) && hash_equals($expected, trim($code));
    }

    public function clearLoginCode(User $user): void
    {
        Cache::forget($this->cacheKey($user));
    }

    public function failedVerifyCount(User $user): int
    {
        return (int) Cache::get($this->failedVerifyCacheKey($user), 0);
    }

    public function isVerifyLockedOut(User $user): bool
    {
        return $this->failedVerifyCount($user) >= max(1, (int) config('hrms_auth.two_factor_max_attempts', 5));
    }

    public function recordFailedVerify(User $user): void
    {
        $key = $this->failedVerifyCacheKey($user);
        $minutes = max(1, (int) config('hrms_auth.two_factor_lockout_minutes', 15));
        Cache::put($key, $this->failedVerifyCount($user) + 1, now()->addMinutes($minutes));
    }

    public function clearFailedVerifies(User $user): void
    {
        Cache::forget($this->failedVerifyCacheKey($user));
    }

    protected function failedVerifyCacheKey(User $user): string
    {
        return 'two_factor_failures:'.$user->id;
    }

    protected function cacheKey(User $user): string
    {
        return 'two_factor_login:' . $user->id;
    }
}
