<?php

namespace SoftminalB2B\Pricing;

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

use SoftminalB2B\Models\Group;
use SoftminalB2B\Registration\Applicant;

/**
 * Who is buying: user -> company? -> group.
 *
 * Resolved once per user per request and memoized, because the resolver is
 * called for every product on a shop page and this lookup would otherwise
 * repeat hundreds of times.
 */
class Context
{
    /** @var array<int, self> */
    private static array $memo = [];

    private int $userId;
    private ?int $groupId;
    private ?int $companyId;
    private ?string $groupSlug;

    /** @var array<string, mixed> */
    private array $groupSettings;

    private function __construct(int $userId, ?int $groupId, ?int $companyId, ?string $groupSlug, array $groupSettings)
    {
        $this->userId        = $userId;
        $this->groupId       = $groupId;
        $this->companyId     = $companyId;
        $this->groupSlug     = $groupSlug;
        $this->groupSettings = $groupSettings;
    }

    public static function forUser(?int $userId = null): self
    {
        $userId = $userId ?? get_current_user_id();

        /**
         * Lets "view as customer" (FEATURES A7) swap the buyer whose prices are
         * resolved, without touching authentication.
         *
         * @param int $userId
         */
        $userId = (int) apply_filters('smb2b_pricing_context_user', $userId);
        $userId = max(0, $userId);

        if (isset(self::$memo[$userId])) {
            return self::$memo[$userId];
        }

        return self::$memo[$userId] = self::build($userId);
    }

    private static function build(int $userId): self
    {
        if ($userId <= 0) {
            return new self(0, null, null, null, []);
        }

        // An applicant awaiting approval sees the shop exactly as a guest does
        // (FLOWS §2). B2B prices must never leak before approval, and a group
        // can be assigned before the decision is recorded, so this check comes
        // first and overrides any assignment.
        if (Applicant::isPending($userId) || Applicant::isRejected($userId)) {
            return new self($userId, null, null, null, []);
        }

        /**
         * Company accounts are a Pro feature, so core never reads those tables.
         * Pro answers this with `['company_id' => int, 'group_id' => int|null]`
         * — a company's group wins over the buyer's own assignment (FEATURES A1:
         * "company membership can override"). Free always resolves to null and
         * costs no query.
         *
         * @param array{company_id:int, group_id:int|null}|null $membership
         * @param int                                           $userId
         */
        $membership = apply_filters('smb2b_pricing_context_company', null, $userId);

        $companyId = null;
        $groupId   = null;

        if (is_array($membership) && !empty($membership['company_id'])) {
            $companyId = (int) $membership['company_id'];

            if (!empty($membership['group_id'])) {
                $groupId = (int) $membership['group_id'];
            }
        }

        if ($groupId === null) {
            $group = Group::forUser($userId);
            if ($group) {
                $groupId = (int) $group->id;
            }
        }

        /**
         * Lets "view as" (A7) preview the shop as a member of a given group
         * without touching the viewer's real assignment. Return a positive
         * group id to force it, null to leave the resolution alone, or 0 to
         * clear the group (simulate retail).
         *
         * @param int|null $groupId The resolved group (company override,
         *                          assignment or none).
         * @param int      $userId
         */
        $forced = apply_filters('smb2b_pricing_context_group', $groupId, $userId);

        if ($forced !== null) {
            $groupId = is_numeric($forced) && (int) $forced > 0 ? (int) $forced : null;
        }

        $slug     = null;
        $settings = [];

        if ($groupId !== null) {
            $group = Group::find($groupId);

            if ($group) {
                $slug     = (string) $group->slug;
                $settings = is_array($group->settings) ? $group->settings : [];
            } else {
                // Group was deleted but the assignment lingers — treat as retail.
                $groupId = null;
            }
        }

        return new self($userId, $groupId, $companyId, $slug, $settings);
    }

    public function userId(): int
    {
        return $this->userId;
    }

    public function groupId(): ?int
    {
        return $this->groupId;
    }

    public function companyId(): ?int
    {
        return $this->companyId;
    }

    public function groupSlug(): ?string
    {
        return $this->groupSlug;
    }

    /**
     * @return array<string, mixed>
     */
    public function groupSettings(): array
    {
        return $this->groupSettings;
    }

    /**
     * True for guests and for logged-in customers with no B2B group — the
     * resolver short-circuits to Woo's own price for these.
     */
    public function isRetail(): bool
    {
        return $this->groupId === null;
    }

    public function allowsCoupons(): bool
    {
        return empty($this->groupSettings['disallow_coupons']);
    }

    /**
     * Part of the price cache key, so two different buyers can never share a
     * cached price.
     */
    public function cacheSegment(): string
    {
        return sprintf('g%s:c%s', $this->groupId ?? 0, $this->companyId ?? 0);
    }

    /**
     * Call after anything that changes group/company assignment mid-request
     * (the "view as customer" preview does exactly this).
     */
    public static function flush(?int $userId = null): void
    {
        if ($userId === null) {
            self::$memo = [];
            return;
        }

        unset(self::$memo[(int) $userId]);
    }
}
