<?php

declare(strict_types=1);

namespace App\Services;

use App\Core\Database;
use PDO;

/**
 * PasswordPolicyService
 *
 * Everything about "is this password acceptable right now" for a given
 * user lives here: complexity rules (config/security.php), reuse
 * against password_histories, and expiration based on
 * users.password_changed_at. AuthService and the (future) profile
 * password-change controller both call into this rather than
 * duplicating the rules.
 */
final class PasswordPolicyService
{
    /** @return string[] Validation error messages; empty array = password is acceptable. */
    public function validateComplexity(string $password): array
    {
        $policy = config('security.password');
        $errors = [];

        if (mb_strlen($password) < $policy['min_length']) {
            $errors[] = "Password must be at least {$policy['min_length']} characters.";
        }
        if ($policy['require_upper'] && !preg_match('/[A-Z]/', $password)) {
            $errors[] = 'Password must include at least one uppercase letter.';
        }
        if ($policy['require_lower'] && !preg_match('/[a-z]/', $password)) {
            $errors[] = 'Password must include at least one lowercase letter.';
        }
        if ($policy['require_number'] && !preg_match('/[0-9]/', $password)) {
            $errors[] = 'Password must include at least one number.';
        }
        if ($policy['require_symbol'] && !preg_match('/[^A-Za-z0-9]/', $password)) {
            $errors[] = 'Password must include at least one symbol.';
        }

        return $errors;
    }

    /** True if this password matches one of the user's last N (config: history_limit) passwords. */
    public function isReused(int $userId, string $newPassword): bool
    {
        $limit = config('security.password.history_limit');

        if ($limit <= 0) {
            return false;
        }

        $stmt = Database::connection()->prepare(
            'SELECT password_hash FROM password_histories WHERE user_id = :id ORDER BY created_at DESC LIMIT :limit'
        );
        $stmt->bindValue('id', $userId, PDO::PARAM_INT);
        $stmt->bindValue('limit', $limit, PDO::PARAM_INT);
        $stmt->execute();

        foreach ($stmt->fetchAll(PDO::FETCH_COLUMN) as $oldHash) {
            if (password_verify($newPassword, $oldHash)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Hashes and commits a new password: updates users.password_hash +
     * password_changed_at, records it in password_histories, and prunes
     * history beyond the configured limit. Does NOT validate complexity
     * or reuse — call validateComplexity()/isReused() first.
     */
    public function commit(int $userId, string $newPlaintextPassword): void
    {
        $policy = config('security.password');
        $hash = password_hash($newPlaintextPassword, $policy['hash_algo'], $policy['hash_options']);
        $pdo = Database::connection();

        $pdo->prepare('UPDATE users SET password_hash = :hash, password_changed_at = NOW() WHERE id = :id')
            ->execute(['hash' => $hash, 'id' => $userId]);

        $pdo->prepare('INSERT INTO password_histories (user_id, password_hash, created_at) VALUES (:id, :hash, NOW())')
            ->execute(['id' => $userId, 'hash' => $hash]);

        $this->pruneHistory($userId, $policy['history_limit']);
    }

    private function pruneHistory(int $userId, int $keep): void
    {
        $pdo = Database::connection();

        $stmt = $pdo->prepare('SELECT id FROM password_histories WHERE user_id = :id ORDER BY created_at DESC LIMIT 1000');
        $stmt->execute(['id' => $userId]);
        $ids = $stmt->fetchAll(PDO::FETCH_COLUMN);

        $toDelete = array_slice($ids, $keep);
        if ($toDelete === []) {
            return;
        }

        $placeholders = implode(',', array_fill(0, count($toDelete), '?'));
        $pdo->prepare("DELETE FROM password_histories WHERE id IN ({$placeholders})")->execute($toDelete);
    }

    /** @return array{expired:bool, expires_in_days:?int} */
    public function expiryStatus(?string $passwordChangedAt): array
    {
        $expiryDays = config('security.password.expiry_days');

        if ($expiryDays === null || $passwordChangedAt === null) {
            return ['expired' => false, 'expires_in_days' => null];
        }

        $changed = new \DateTimeImmutable($passwordChangedAt);
        $expiresAt = $changed->modify("+{$expiryDays} days");
        $now = new \DateTimeImmutable();

        $diffDays = (int) $now->diff($expiresAt)->format('%r%a');

        return [
            'expired' => $now >= $expiresAt,
            'expires_in_days' => $diffDays,
        ];
    }

    public function isNearingExpiry(?string $passwordChangedAt): bool
    {
        $status = $this->expiryStatus($passwordChangedAt);
        $warningDays = config('security.password.expiry_warning_days');

        return !$status['expired'] && $status['expires_in_days'] !== null && $status['expires_in_days'] <= $warningDays;
    }
}
