<?php

namespace SoftminalB2B\Registration;

if (!defined('ABSPATH')) {
    exit;
}

use SoftminalB2B\Models\Group;

/**
 * A B2B applicant: a real WordPress user carrying an approval status and the
 * business details they registered with (FEATURES C1/C2).
 *
 * Stored in user meta rather than a table, matching the decision already taken
 * for group membership: these are user-scoped facts at user cardinality, and
 * WordPress indexes user meta lookups perfectly well at that size. The custom
 * tables are for pricing, companies, quotes and credit.
 */
class Applicant
{
    public const META_STATUS   = 'smb2b_status';
    public const META_BUSINESS = 'smb2b_business';
    public const META_APPLIED  = 'smb2b_applied_at';
    public const META_DECIDED  = 'smb2b_decided_at';
    public const META_REASON   = 'smb2b_decision_reason';

    public const STATUS_PENDING  = 'pending';
    public const STATUS_APPROVED = 'approved';
    public const STATUS_REJECTED = 'rejected';

    /** @var array<string, int>|null Per-request memo for countByStatuses(). */
    private static $statusCounts = null;

    /**
     * The business fields the free version ships. B2BKing's free tier omitting
     * these is the single loudest complaint against it, so they are core, not
     * an upsell (FEATURES C1).
     *
     * @return array<string, array{label:string, required:bool, type:string}>
     */
    public static function fields(): array
    {
        /**
         * Filter the business fields on the B2B registration form.
         *
         * Each entry: key => [label, required(bool), type(text|tel|textarea)].
         * Pro's custom field builder registers through this same filter.
         *
         * @param array<string, array{label:string, required:bool, type:string}> $fields
         */
        return apply_filters('smb2b_registration_fields', [
            'company_name' => [
                'label'    => __('Company name', 'softminal-b2b-wholesale-for-woocommerce'),
                'required' => true,
                'type'     => 'text',
            ],
            'vat_number' => [
                'label'    => __('VAT or tax ID', 'softminal-b2b-wholesale-for-woocommerce'),
                'required' => false,
                'type'     => 'text',
            ],
            'phone' => [
                'label'    => __('Phone', 'softminal-b2b-wholesale-for-woocommerce'),
                'required' => true,
                'type'     => 'tel',
            ],
            'address' => [
                'label'    => __('Business address', 'softminal-b2b-wholesale-for-woocommerce'),
                'required' => false,
                'type'     => 'textarea',
            ],
        ]);
    }

    public static function statusOf(int $userId): ?string
    {
        if ($userId <= 0) {
            return null;
        }

        $status = get_user_meta($userId, self::META_STATUS, true);

        return $status === '' ? null : (string) $status;
    }

    /**
     * True only for applicants still waiting. Customers who never applied have
     * no status at all and are ordinary retail shoppers, not pending.
     */
    public static function isPending(int $userId): bool
    {
        return self::statusOf($userId) === self::STATUS_PENDING;
    }

    public static function isApproved(int $userId): bool
    {
        return self::statusOf($userId) === self::STATUS_APPROVED;
    }

    public static function isRejected(int $userId): bool
    {
        return self::statusOf($userId) === self::STATUS_REJECTED;
    }

    /**
     * @param array<string, string> $business
     */
    public static function markPending(int $userId, array $business): void
    {
        update_user_meta($userId, self::META_STATUS, self::STATUS_PENDING);
        update_user_meta($userId, self::META_BUSINESS, $business);
        update_user_meta($userId, self::META_APPLIED, current_time('mysql'));
        delete_user_meta($userId, self::META_DECIDED);
        delete_user_meta($userId, self::META_REASON);

        self::flushStatusCounts();
    }

    /**
     * Approving assigns the group, which is what actually turns B2B pricing on.
     */
    public static function approve(int $userId, ?int $groupId): bool
    {
        if (!get_userdata($userId)) {
            return false;
        }

        update_user_meta($userId, self::META_STATUS, self::STATUS_APPROVED);
        update_user_meta($userId, self::META_DECIDED, current_time('mysql'));
        delete_user_meta($userId, self::META_REASON);

        Group::assignUser($userId, $groupId);
        self::flushStatusCounts();

        do_action('smb2b_registration_approved', $userId, $groupId);

        return true;
    }

    public static function reject(int $userId, string $reason = ''): bool
    {
        if (!get_userdata($userId)) {
            return false;
        }

        update_user_meta($userId, self::META_STATUS, self::STATUS_REJECTED);
        update_user_meta($userId, self::META_DECIDED, current_time('mysql'));
        update_user_meta($userId, self::META_REASON, $reason);

        // A rejected applicant must lose any B2B pricing immediately.
        Group::assignUser($userId, null);
        self::flushStatusCounts();

        do_action('smb2b_registration_rejected', $userId, $reason);

        return true;
    }

    /**
     * @return array<string, string>
     */
    public static function business(int $userId): array
    {
        $business = get_user_meta($userId, self::META_BUSINESS, true);

        return is_array($business) ? $business : [];
    }

    public static function countPending(): int
    {
        return self::countByStatuses()[self::STATUS_PENDING] ?? 0;
    }

    public static function countByStatus(string $status): int
    {
        return self::countByStatuses()[$status] ?? 0;
    }

    /**
     * All applicant status counts in a single grouped query.
     *
     * The dashboard needs pending, approved and rejected together, and used to
     * pay for each with its own WP_User_Query — four scans of the status meta,
     * one of them a duplicate. One GROUP BY answers all of them. `meta_value`
     * is unindexed in core, so the scan is unavoidable; doing it once instead of
     * four times is the win. Memoised per request because the dashboard reads
     * these through more than one path.
     *
     * @return array<string, int> status => count
     */
    public static function countByStatuses(): array
    {
        if (self::$statusCounts !== null) {
            return self::$statusCounts;
        }

        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT meta_value AS status, COUNT(*) AS total
                 FROM {$wpdb->usermeta}
                 WHERE meta_key = %s
                 GROUP BY meta_value",
                self::META_STATUS
            ),
            ARRAY_A
        );

        $counts = [
            self::STATUS_PENDING  => 0,
            self::STATUS_APPROVED => 0,
            self::STATUS_REJECTED => 0,
        ];

        foreach ($rows ?: [] as $row) {
            $counts[(string) $row['status']] = (int) $row['total'];
        }

        return self::$statusCounts = $counts;
    }

    /**
     * Drop the memoised status counts. Called whenever an applicant's status
     * changes so a later read in the same process (a CLI run, a request that
     * both mutates and reports) does not return a stale tally.
     */
    private static function flushStatusCounts(): void
    {
        self::$statusCounts = null;
    }
}
