<?php

namespace auctionforge\inc\model;

use auctionforge\inc\controllers\BidspiritLots;
use auctionforge\inc\helpers\SettingsHelper;
use WP_Term;

/**
 * BidSpirit Shop support.
 *
 * A BidSpirit Shop is a fixed-price storefront that lives on the house's
 * BidSpirit account alongside its auctions. It is NOT an auction: items come
 * back from shop/getShopItems with `auctionId` = 0 and no `itemIndex`, they
 * carry a `shopPrice` instead of a start price, and they are bought outright
 * through shop/purchaseShopItem rather than bid on.
 *
 * To reuse the entire existing lot pipeline — templates, CDN images,
 * categories, site search, pagination, cache purge, SEO — the shop is
 * represented locally as ONE synthetic `auction_number` term carrying
 * {@see self::TERM_META_SHOP_ID} term meta. Everything that renders a lot then
 * works unchanged, and {@see Auction::isAuctionShop()} reports true for that
 * term so the existing buy-now presentation applies.
 *
 * ⚠ This is deliberately SEPARATE from the older "Auctions as shop"
 * (`shop_auction`) setting, which flags a real auction to be *presented* as a
 * shop and sells through items/buyout. Both mechanisms can be live on the same
 * site; they are backed by different BidSpirit data and neither affects the
 * other.
 */
class Shop
{
    /** Master on/off. Off by default — the feature is inert until a house opts in. */
    const SETTING_ENABLED = 'bs_shop_enabled';

    /** Optional display name for the shop term. */
    const SETTING_TITLE = 'bs_shop_title';

    /**
     * Hide items that have already sold from the shop listing.
     *
     * Off by default: a sold item still carries its description and photos, and
     * keeping it visible (marked sold, with the price it made) is useful proof
     * of trade. Unlike an auction there is no "archived" moment after which
     * everything is sold — a shop runs continuously — so this is a plain site
     * preference rather than a state transition.
     */
    const SETTING_HIDE_SOLD = 'bs_shop_hide_sold';

    /**
     * Term meta holding the BidSpirit shopId. Presence of this key is also the
     * marker that identifies a term as a shop rather than an auction.
     */
    const TERM_META_SHOP_ID = 'bs_shop_id';

    /** Currency reported by shop/getShopItems (the shop has its own). */
    const TERM_META_CURRENCY = 'bs_shop_currency';

    /**
     * Base for the synthetic `auction_id` term meta.
     *
     * The shop needs an `auction_id` because InsertLotsBS resolves a staged
     * lot's term with Auction::findAuctionById($lot->getAuctionId()), and that
     * staging column is written with a `%d` mask — so the id MUST be numeric
     * (a string would be cast to 0 and every shop lot would fail to find its
     * term). A BidSpirit shopId lives in its own number space, so shopId 1
     * would collide with auction 1; offsetting into a high range that real
     * BidSpirit auction ids never reach keeps the two apart.
     */
    const AUCTION_ID_BASE = 990000000;

    /** Default slug/name when the house has not set a title. */
    const DEFAULT_TITLE = 'Shop';

    /**
     * Is the BidSpirit Shop feature switched on for this site?
     *
     * @return bool
     */
    public static function isEnabled()
    {
        return SettingsHelper::isChecked(self::SETTING_ENABLED, 'off');
    }

    /**
     * Synthetic, numeric auction_id for a given BidSpirit shopId.
     *
     * @param int|string $shopId
     * @return int
     */
    public static function auctionIdFor($shopId)
    {
        return self::AUCTION_ID_BASE + (int) $shopId;
    }

    /**
     * Is this term the local representation of a BidSpirit Shop?
     *
     * Cheap enough to call from templates — it is a single term-meta read that
     * WordPress caches per request.
     *
     * @param int $termId
     * @return bool
     */
    public static function isShopTerm($termId)
    {
        if (empty($termId)) {
            return false;
        }
        return SettingsHelper::getTermOption($termId, self::TERM_META_SHOP_ID, '') !== '';
    }

    /**
     * BidSpirit shopId behind a term, or '' when the term is not a shop.
     *
     * @param int $termId
     * @return string
     */
    public static function getShopId($termId)
    {
        return SettingsHelper::getTermOption($termId, self::TERM_META_SHOP_ID, '');
    }

    /**
     * Currency for a shop term, falling back to the site-wide setting.
     *
     * @param int $termId
     * @return string
     */
    public static function getCurrency($termId)
    {
        $currency = SettingsHelper::getTermOption($termId, self::TERM_META_CURRENCY, '');
        if ($currency === '') {
            $currency = SettingsHelper::getOption('currency', '$');
        }
        return $currency;
    }

    /**
     * Term ids of every shop on this site (normally one).
     *
     * Used to keep shops out of auction queries. Returns a plain array of ints
     * so it can be dropped straight into a `term_taxonomy_id`/`exclude` arg.
     *
     * @return int[]
     */
    public static function getShopTermIds()
    {
        global $wpdb;

        $ids = $wpdb->get_col($wpdb->prepare(
            "SELECT term_id FROM {$wpdb->termmeta} WHERE meta_key = %s",
            self::TERM_META_SHOP_ID
        ));

        return array_map('intval', (array) $ids);
    }

    /**
     * Find the local term for a BidSpirit shopId.
     *
     * @param int|string $shopId
     * @return WP_Term|false
     */
    public static function findTerm($shopId)
    {
        $terms = get_terms(array(
            'taxonomy'   => BidspiritLots::TAX_AUCTION_TYPE,
            'hide_empty' => false,
            'number'     => 1,
            'meta_query' => array(
                array(
                    'key'   => self::TERM_META_SHOP_ID,
                    'value' => (string) $shopId,
                ),
            ),
        ));

        if ($terms instanceof \WP_Error || empty($terms[0])) {
            return false;
        }

        return $terms[0];
    }

    /**
     * Find or create the synthetic term for a shop, and keep its meta current.
     *
     * The term is given the same shape of meta a real auction carries, so every
     * template path that reads auction meta finds a sane value instead of null.
     * `auction_state` is READY (the state that means "published, accepting
     * activity") and `auction_days` holds a single synthetic day, because
     * several helpers index `auction_days[0]` without checking.
     *
     * @param int|string $shopId
     * @param string     $currency
     * @return WP_Term|false
     */
    public static function ensureTerm($shopId, $currency = '')
    {
        $title = SettingsHelper::getOption(self::SETTING_TITLE, '');
        if (trim($title) === '') {
            $title = self::DEFAULT_TITLE;
        }

        $term = self::findTerm($shopId);

        if (!$term) {
            $slug = sanitize_title($title . '-' . $shopId);

            // A term with this slug may already exist from an earlier run that
            // failed before its meta was written — adopt it rather than erroring.
            $existing = get_term_by('slug', $slug, BidspiritLots::TAX_AUCTION_TYPE);
            if ($existing instanceof WP_Term) {
                $term = $existing;
            } else {
                $created = wp_insert_term($title, BidspiritLots::TAX_AUCTION_TYPE, array(
                    'slug' => $slug,
                ));

                if ($created instanceof \WP_Error || empty($created['term_id'])) {
                    return false;
                }

                $term = get_term($created['term_id'], BidspiritLots::TAX_AUCTION_TYPE);
                if (!($term instanceof WP_Term)) {
                    return false;
                }
            }
        }

        self::writeTermMeta($term->term_id, $shopId, $currency, $title);

        return $term;
    }

    /**
     * Write the auction-shaped meta a shop term needs.
     *
     * Only writes on change, mirroring Auction::updateOptionMeta(), so a
     * no-change sync does not churn term meta or trigger cache purges.
     *
     * @param int        $termId
     * @param int|string $shopId
     * @param string     $currency
     * @param string     $title
     * @return void
     */
    private static function writeTermMeta($termId, $shopId, $currency, $title)
    {
        $day = new \stdClass();
        $day->date = date('Y-m-d');
        $day->time = '00:00:00';
        $day->index = 1;
        $day->ended = false;
        $day->firstLotIndex = 0;
        $day->lastLotIndex = 0;

        $meta = array(
            self::TERM_META_SHOP_ID => (string) $shopId,
            'auction_id'            => self::auctionIdFor($shopId),
            'auction_name'          => $title,
            'auction_number'        => self::auctionIdFor($shopId),
            'auction_state'         => Auction::STATE_READY,
            'auction_days'          => array($day),
            'auction_address'       => '',
            'auction_info'          => '',
            // 0 deliberately. shop/purchaseShopItem requires `price` to equal
            // shopPrice exactly, so adding a buyer's premium on top would show
            // the customer a total that is not what they are charged.
            'auction_commission'    => 0,
            'auction_commissionInfo' => '',
            'auction_docs'          => '',
            'auction_date'          => $day->date,
            'auction_displayStartPriceInCatalog' => true,
            'auction_showEstimatedPriceInItemsList' => false,
            'auction_automaticAuction' => Auction::toString(false),
        );

        if ($currency !== '') {
            $meta[self::TERM_META_CURRENCY] = $currency;
        }

        foreach ($meta as $key => $value) {
            $previous = SettingsHelper::getTermOption($termId, $key);
            if ($value != $previous) {
                update_term_meta($termId, $key, $value, $previous);
            }
        }
    }

    /**
     * Keep shop terms out of the auction lists.
     *
     * A shop lives in the `auction_number` taxonomy so it can reuse the lot
     * rendering stack, which means Auction::getAuctions() would otherwise pick
     * it up and show it on the auctions/archive pages and feed it to the normal
     * catalogue sync. AuctionForge already exposes the two filters used here
     * for exactly this purpose.
     *
     * Registered unconditionally: sites with no shop have no terms carrying the
     * marker, so the extra clause matches everything and costs one JOIN.
     *
     * @return void
     */
    public static function register()
    {
        $exclude = function ($args) {
            if (empty($args['meta_query']) || !is_array($args['meta_query'])) {
                $args['meta_query'] = array('relation' => 'AND');
            }

            $args['meta_query'][] = array(
                'key'     => self::TERM_META_SHOP_ID,
                'compare' => 'NOT EXISTS',
            );

            return $args;
        };

        add_filter('auctionforge_query_auction_params', $exclude);
        add_filter('auctionforge_query_auction_params_sync', $exclude);
    }
}
