<?php

namespace SoftminalB2B\Models;

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

use SoftminalB2B\Models\Concerns\InvalidatesPricing;

/**
 * A B2B customer group. Membership lives in user meta `smb2b_group_id`
 * (one group per user), not in a pivot table.
 *
 * @property int|null                $id
 * @property string                  $name
 * @property string                  $slug
 * @property array<string, mixed>|null $settings
 * @property string                  $created_at
 * @property string                  $updated_at
 */
class Group extends Model
{
    use InvalidatesPricing;

    protected $table = 'groups';
    protected $timestamps = true;

    protected $fillable = [
        'name',
        'slug',
        'settings',
    ];

    protected $casts = [
        'settings' => 'json',
    ];

    public const USER_META_KEY = 'smb2b_group_id';

    public function priceRules()
    {
        return $this->hasMany(PriceRule::class, 'group_id');
    }

    public static function findBySlug(string $slug): ?self
    {
        return static::where('slug', $slug)->first();
    }

    /**
     * The group a user belongs to, or null for retail customers.
     */
    public static function forUser(int $userId): ?self
    {
        if ($userId <= 0) {
            return null;
        }

        $groupId = (int) get_user_meta($userId, self::USER_META_KEY, true);

        return $groupId > 0 ? static::find($groupId) : null;
    }

    public static function assignUser(int $userId, ?int $groupId): bool
    {
        if ($userId <= 0) {
            return false;
        }

        self::$allMemberIds = null; // membership is changing; drop the request memo

        if ($groupId === null) {
            delete_user_meta($userId, self::USER_META_KEY);

            // The user is now unassigned (retail) whether or not a row existed,
            // so report success rather than delete_user_meta's "was there a row".
            return true;
        }

        // update_user_meta returns false both on failure and when the value is
        // unchanged. Treat "already in this group" as success, or a bulk move
        // that re-includes existing members would report fewer moves than it
        // made (A: the assign endpoint counts on this return).
        if ((int) get_user_meta($userId, self::USER_META_KEY, true) === $groupId) {
            return true;
        }

        return (bool) update_user_meta($userId, self::USER_META_KEY, $groupId);
    }

    /**
     * How many customers are in one group, without loading their ids just to
     * count them.
     */
    public static function memberCount(int $groupId): int
    {
        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        return (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(*) FROM {$wpdb->usermeta} WHERE meta_key = %s AND meta_value = %s",
                self::USER_META_KEY,
                (string) $groupId
            )
        );
    }

    /**
     * Every user id in one group. Used when a group is deleted and its members
     * must be moved back to retail.
     *
     * @return array<int, int>
     */
    public static function memberIds(int $groupId): array
    {
        $query = new \WP_User_Query([
            'meta_key'   => self::USER_META_KEY, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
            'meta_value' => (string) $groupId, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
            'fields'     => 'ID',
            'number'     => -1,
        ]);

        return array_map('absint', $query->get_results());
    }

    /** Request-scoped memo for allMemberIds(); the dashboard reads it several times per load. */
    private static ?array $allMemberIds = null;

    /**
     * Every customer id assigned to any group (i.e. every B2B customer), for
     * aggregating their WooCommerce orders. Retail customers have no meta row
     * and so are naturally excluded.
     *
     * Memoized per request: the dashboard hits this from all(), revenueStats()
     * and salesRange() on a single load, and the underlying full-usermeta scan
     * is the same every time. Busted by assignUser() when membership changes.
     *
     * @return array<int, int>
     */
    public static function allMemberIds(): array
    {
        if (self::$allMemberIds !== null) {
            return self::$allMemberIds;
        }

        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $ids = $wpdb->get_col(
            $wpdb->prepare(
                "SELECT DISTINCT user_id FROM {$wpdb->usermeta} WHERE meta_key = %s AND meta_value > 0",
                self::USER_META_KEY
            )
        );

        return self::$allMemberIds = array_values(array_filter(array_map('absint', $ids)));
    }

    /**
     * Member count plus the first few member ids for every group at once, in a
     * single grouped scan of the membership meta: group id => [total, ids].
     * GROUP_CONCAT keeps the preview ids to a handful per group (default
     * group_concat_max_len of 1024 bytes is ample for `$previewLimit` ids).
     *
     * @return array<int, array{total: int, ids: array<int, int>}>
     */
    public static function memberSummary(int $previewLimit = 4): array
    {
        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT meta_value AS group_id, COUNT(*) AS total,
                        SUBSTRING_INDEX(GROUP_CONCAT(user_id ORDER BY user_id), ',', %d) AS preview_ids
                 FROM {$wpdb->usermeta}
                 WHERE meta_key = %s
                 GROUP BY meta_value",
                $previewLimit,
                self::USER_META_KEY
            ),
            ARRAY_A
        );

        $summary = [];

        foreach ($rows ?: [] as $row) {
            $ids = array_values(array_filter(array_map('absint', explode(',', (string) $row['preview_ids']))));

            $summary[(int) $row['group_id']] = [
                'total' => (int) $row['total'],
                'ids'   => $ids,
            ];
        }

        return $summary;
    }

    /**
     * Customers in a group, paginated and optionally searched — a wholesale
     * group can hold thousands, so this never loads them all at once.
     *
     * @param array{page?: int, per_page?: int, search?: string} $options
     * @return array{users: array<int, \WP_User>, total: int}
     */
    public static function members(int $groupId, array $options = []): array
    {
        $page    = max(1, (int) ($options['page'] ?? 1));
        $perPage = min(100, max(1, (int) ($options['per_page'] ?? 20)));
        $search  = trim((string) ($options['search'] ?? ''));

        $args = [
            'meta_key'   => self::USER_META_KEY, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
            'meta_value' => (string) $groupId, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
            'number'     => $perPage,
            'paged'      => $page,
            'orderby'    => 'display_name',
            'order'      => 'ASC',
        ];

        if ($search !== '') {
            $args['search']         = '*' . $search . '*';
            $args['search_columns'] = ['user_login', 'user_email', 'display_name'];
        }

        $query = new \WP_User_Query($args);

        return [
            'users' => $query->get_results(),
            'total' => (int) $query->get_total(),
        ];
    }

    /**
     * The first few customers in a group, for the avatar preview on the groups
     * list. Used when a single group is presented on its own; the list endpoint
     * batch-loads previews via memberSummary() instead.
     *
     * @return array<int, \WP_User>
     */
    public static function previewMembers(int $groupId, int $limit = 4): array
    {
        $query = new \WP_User_Query([
            'meta_key'   => self::USER_META_KEY, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
            'meta_value' => (string) $groupId, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
            'number'     => $limit,
            'orderby'    => 'ID',
            'order'      => 'ASC',
        ]);

        return $query->get_results();
    }

    /**
     * Load a set of users by id in one query, keyed by id — for batch-resolving
     * the preview ids from memberSummary() across several groups at once.
     *
     * @param array<int, int> $ids
     * @return array<int, \WP_User>
     */
    public static function usersByIds(array $ids): array
    {
        $ids = array_values(array_unique(array_filter(array_map('absint', $ids))));

        if (empty($ids)) {
            return [];
        }

        $query = new \WP_User_Query([
            'include' => $ids,
            'number'  => count($ids),
        ]);

        $users = [];

        foreach ($query->get_results() as $user) {
            $users[(int) $user->ID] = $user;
        }

        return $users;
    }

    /**
     * Slug unique against the table's UNIQUE KEY, suffixing on collision.
     */
    public static function uniqueSlug(string $name, ?int $ignoreId = null): string
    {
        $base = sanitize_title($name);
        if ($base === '') {
            $base = 'group';
        }

        $slug = $base;
        $suffix = 2;

        while (true) {
            $existing = static::where('slug', $slug)->first();

            if (!$existing || ($ignoreId !== null && (int) $existing->id === $ignoreId)) {
                return $slug;
            }

            $slug = $base . '-' . $suffix;
            $suffix++;
        }
    }
}
