<?php

namespace Modules\Tpin\Services;

use App\Models\ActivityLogModel;
use Modules\Auth\Models\UserModel;
use Modules\Auth\Entities\UserEntity;

/**
 * Reusable TPIN (transaction PIN) helpers.
 * Tasks that need step-up confirmation call verify() / assertConfirmedForAction().
 */
class TpinService
{
    public const MIN_LENGTH = 4;
    public const MAX_LENGTH = 6;
    public const MAX_FAILED = 5;
    public const LOCK_MINUTES = 15;

    protected UserModel $users;
    protected ActivityLogModel $activity;

    public function __construct()
    {
        $this->users    = new UserModel();
        $this->activity = new ActivityLogModel();
    }

    public function hasTpin(int $userId): bool
    {
        $user = $this->users->find($userId);

        return $user !== null && ! empty($user->tpin_hash);
    }

    public function isRequired(int $userId): bool
    {
        if (! $this->companyTpinEnabled()) {
            return false;
        }

        $user = $this->users->find($userId);

        return $user !== null && (bool) $user->tpin_required;
    }

    /**
     * True when this user must enter TPIN before a sensitive action.
     */
    public function userMustConfirm(int $userId): bool
    {
        return $this->isRequired($userId) && $this->hasTpin($userId);
    }

    /**
     * Server-side gate for protected actions.
     * Expects a prior successful POST /tpin/verify (sets session tpin_verified_at).
     * Consumes the session flag on success (one-shot).
     *
     * @return array{ok: bool, error?: string, redirect?: string}
     */
    public function assertConfirmedForAction(int $userId): array
    {
        if (! $this->isRequired($userId)) {
            return ['ok' => true];
        }

        if (! $this->hasTpin($userId)) {
            return [
                'ok'       => false,
                'error'    => 'Set your TPIN before continuing. An administrator requires TPIN confirmation for your account.',
                'redirect' => '/profile/tpin',
            ];
        }

        $verifiedAt = (int) (session()->get('tpin_verified_at') ?? 0);
        if ($verifiedAt > 0 && (time() - $verifiedAt) <= 300) {
            session()->remove('tpin_verified_at');

            return ['ok' => true];
        }

        return [
            'ok'    => false,
            'error' => 'TPIN confirmation is required for this action.',
        ];
    }

    public function isLocked(int $userId): bool
    {
        $user = $this->users->find($userId);
        if (! $user || empty($user->tpin_locked_until)) {
            return false;
        }

        return strtotime((string) $user->tpin_locked_until) > time();
    }

    /**
     * Validate PIN format (digits only, length bounds).
     *
     * @return list<string>
     */
    public function validateFormat(string $tpin): array
    {
        $errors = [];

        if (! preg_match('/^\d+$/', $tpin)) {
            $errors[] = 'TPIN must contain digits only.';
        }

        $len = strlen($tpin);
        if ($len < self::MIN_LENGTH || $len > self::MAX_LENGTH) {
            $errors[] = 'TPIN must be ' . self::MIN_LENGTH . '–' . self::MAX_LENGTH . ' digits.';
        }

        // Reject trivial sequences like 0000 / 1234
        if (preg_match('/^(\d)\1+$/', $tpin)) {
            $errors[] = 'TPIN cannot be all the same digit.';
        }

        return $errors;
    }

    /**
     * Set or replace the user's TPIN. Requires account password confirmation.
     *
     * @return array{success: bool, error?: string}
     */
    public function setTpin(int $userId, string $tpin, string $tpinConfirm, string $currentPassword): array
    {
        $user = $this->users->find($userId);
        if (! $user) {
            return ['success' => false, 'error' => 'User not found.'];
        }

        if (! password_verify($currentPassword, (string) $user->password_hash)) {
            return ['success' => false, 'error' => 'Current password is incorrect.'];
        }

        if ($tpin !== $tpinConfirm) {
            return ['success' => false, 'error' => 'TPIN confirmation does not match.'];
        }

        $formatErrors = $this->validateFormat($tpin);
        if ($formatErrors) {
            return ['success' => false, 'error' => implode(' ', $formatErrors)];
        }

        $this->users->update($userId, [
            'tpin_hash'         => password_hash($tpin, PASSWORD_BCRYPT, ['cost' => 12]),
            'tpin_set_at'       => date('Y-m-d H:i:s'),
            'tpin_failed_count' => 0,
            'tpin_locked_until' => null,
        ]);

        $this->activity->log('tpin', 'set', $userId, "User #{$userId} set TPIN", (string) $userId);

        return ['success' => true];
    }

    /**
     * Change TPIN using the existing PIN (no password needed).
     *
     * @return array{success: bool, error?: string}
     */
    public function changeTpin(int $userId, string $currentTpin, string $newTpin, string $newTpinConfirm): array
    {
        $user = $this->users->find($userId);
        if (! $user || empty($user->tpin_hash)) {
            return ['success' => false, 'error' => 'No TPIN is set. Create one first.'];
        }

        if ($this->isLocked($userId)) {
            return ['success' => false, 'error' => 'TPIN is temporarily locked. Try again later.'];
        }

        if (! password_verify($currentTpin, (string) $user->tpin_hash)) {
            $this->recordFailure($userId);

            return ['success' => false, 'error' => 'Current TPIN is incorrect.'];
        }

        if ($newTpin !== $newTpinConfirm) {
            return ['success' => false, 'error' => 'New TPIN confirmation does not match.'];
        }

        $formatErrors = $this->validateFormat($newTpin);
        if ($formatErrors) {
            return ['success' => false, 'error' => implode(' ', $formatErrors)];
        }

        if ($currentTpin === $newTpin) {
            return ['success' => false, 'error' => 'New TPIN must be different from the current one.'];
        }

        $this->users->update($userId, [
            'tpin_hash'         => password_hash($newTpin, PASSWORD_BCRYPT, ['cost' => 12]),
            'tpin_set_at'       => date('Y-m-d H:i:s'),
            'tpin_failed_count' => 0,
            'tpin_locked_until' => null,
        ]);

        $this->activity->log('tpin', 'changed', $userId, "User #{$userId} changed TPIN", (string) $userId);

        return ['success' => true];
    }

    /**
     * User removes their own TPIN (requires current TPIN).
     *
     * @return array{success: bool, error?: string}
     */
    public function clearOwnTpin(int $userId, string $currentTpin): array
    {
        $verify = $this->verify($userId, $currentTpin);
        if (! $verify['success']) {
            return $verify;
        }

        $this->users->update($userId, [
            'tpin_hash'         => null,
            'tpin_set_at'       => null,
            'tpin_failed_count' => 0,
            'tpin_locked_until' => null,
        ]);

        $this->activity->log('tpin', 'cleared', $userId, "User #{$userId} cleared own TPIN", (string) $userId);

        return ['success' => true];
    }

    /**
     * Admin clears a user's TPIN (e.g. forgotten PIN).
     */
    public function adminClearTpin(int $userId, ?int $actingUserId): void
    {
        $this->users->update($userId, [
            'tpin_hash'         => null,
            'tpin_set_at'       => null,
            'tpin_failed_count' => 0,
            'tpin_locked_until' => null,
        ]);

        $this->activity->log(
            'tpin',
            'cleared_by_admin',
            $actingUserId,
            "Admin cleared TPIN for user #{$userId}",
            (string) $userId
        );
    }

    /**
     * Admin toggles whether this user must confirm with TPIN on protected actions.
     */
    public function setRequired(int $userId, bool $required, ?int $actingUserId): void
    {
        $this->users->update($userId, ['tpin_required' => $required ? 1 : 0]);

        $this->activity->log(
            'tpin',
            $required ? 'required_enabled' : 'required_disabled',
            $actingUserId,
            ($required ? 'Enabled' : 'Disabled') . " TPIN requirement for user #{$userId}",
            (string) $userId
        );
    }

    /**
     * Verify a TPIN for step-up confirmation. Use from any future task.
     *
     * @return array{success: bool, error?: string, locked?: bool}
     */
    public function verify(int $userId, string $tpin): array
    {
        $user = $this->users->find($userId);
        if (! $user) {
            return ['success' => false, 'error' => 'User not found.'];
        }

        if (empty($user->tpin_hash)) {
            return ['success' => false, 'error' => 'No TPIN is set. Set one in your profile first.'];
        }

        if ($this->isLocked($userId)) {
            return [
                'success' => false,
                'locked'  => true,
                'error'   => 'TPIN is temporarily locked after too many failed attempts. Try again later.',
            ];
        }

        if (! password_verify($tpin, (string) $user->tpin_hash)) {
            $locked = $this->recordFailure($userId);

            return [
                'success' => false,
                'locked'  => $locked,
                'error'   => $locked
                    ? 'Too many failed attempts. TPIN is locked temporarily.'
                    : 'Incorrect TPIN.',
            ];
        }

        $this->users->update($userId, [
            'tpin_failed_count' => 0,
            'tpin_locked_until' => null,
        ]);

        // Short-lived flag for optional TpinFilter on future task routes.
        session()->set('tpin_verified_at', time());

        return ['success' => true];
    }

    /**
     * Status payload for profile / admin screens.
     *
     * @return array{has_tpin: bool, required: bool, locked: bool, set_at: ?string}
     */
    public function statusFor(int $userId): array
    {
        $user = $this->users->find($userId);

        if (! $user) {
            return ['has_tpin' => false, 'required' => false, 'locked' => false, 'set_at' => null];
        }

        return [
            'has_tpin' => ! empty($user->tpin_hash),
            'required' => (bool) $user->tpin_required,
            'locked'   => $this->isLocked($userId),
            'set_at'   => $user->tpin_set_at ? (string) $user->tpin_set_at : null,
        ];
    }

    /**
     * @return bool True if the account is now locked.
     */
    protected function recordFailure(int $userId): bool
    {
        /** @var UserEntity|null $user */
        $user = $this->users->find($userId);
        if (! $user) {
            return false;
        }

        $count = (int) $user->tpin_failed_count + 1;
        $data  = ['tpin_failed_count' => $count];

        $policy = $this->companyPolicy();
        $locked = false;
        if ($count >= $policy['max_failed']) {
            $data['tpin_locked_until'] = date('Y-m-d H:i:s', time() + $policy['lockout_minutes'] * 60);
            $data['tpin_failed_count'] = 0;
            $locked = true;
        }

        $this->users->update($userId, $data);

        return $locked;
    }

    /**
     * @return array{enabled: bool, required_by_default: bool, max_failed: int, lockout_minutes: int}
     */
    public function companyPolicy(): array
    {
        $settings = (new \App\Services\CompanySettingsService())->get();

        return [
            'enabled'            => (bool) ($settings['tpin_enabled'] ?? true),
            'required_by_default'=> (bool) ($settings['tpin_required_by_default'] ?? false),
            'max_failed'         => max(3, min(10, (int) ($settings['tpin_max_failed_attempts'] ?? self::MAX_FAILED))),
            'lockout_minutes'    => max(5, min(60, (int) ($settings['tpin_lockout_minutes'] ?? self::LOCK_MINUTES))),
        ];
    }

    private function companyTpinEnabled(): bool
    {
        return $this->companyPolicy()['enabled'];
    }
}
