<?php

namespace SoftminalB2B;

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

/**
 * Schema owner for the whole suite.
 *
 * Core runs every migration, including the Pro add-on's tables — Pro never
 * migrates, which removes core<->Pro migration ordering as a failure mode.
 */
class Install
{
    private static $wpdb;
    private static $charset_collate;

    private const VERSION_OPTION = 'smb2b_db_version';

    private static function init(): void
    {
        global $wpdb;
        self::$wpdb = $wpdb;
        self::$charset_collate = $wpdb->get_charset_collate();
    }

    public static function activate(): void
    {
        self::init();

        if (!self::createTables()) {
            wp_die(esc_html__('Could not install B2B Suite tables. Please check your error logs.', 'softminal-b2b-wholesale-for-woocommerce'));
        }

        update_option(self::VERSION_OPTION, SMB2B_DB_VERSION);

        // Register the My Account endpoints then flush rewrite rules once, so
        // their pretty URLs resolve without a manual permalink save.
        \SoftminalB2B\Storefront\PurchaseListsAccount::registerEndpoint();
        \SoftminalB2B\Storefront\MessageCentre::registerEndpoint();
        flush_rewrite_rules();
    }

    /**
     * Runs on every load so a plugin updated via file copy (no activation hook)
     * still gets its schema brought forward.
     */
    public static function maybeUpgrade(): void
    {
        if (get_option(self::VERSION_OPTION) === SMB2B_DB_VERSION) {
            return;
        }

        self::init();

        if (self::createTables()) {
            update_option(self::VERSION_OPTION, SMB2B_DB_VERSION);
        }
    }

    public static function tableName(string $table): string
    {
        global $wpdb;
        return $wpdb->prefix . SMB2B_TABLE_PREFIX . $table;
    }

    /**
     * Every table the suite owns, in dependency-free order (all FKs are logical).
     *
     * @return string[]
     */
    public static function tables(): array
    {
        return [
            'groups',
            'price_rules',
            'quantity_rules',
            'bogo_rules',
            'companies',
            'company_users',
            'invitations',
            'quotes',
            'quote_items',
            'quote_messages',
            'credit_accounts',
            'credit_transactions',
            'purchase_lists',
            'conversations',
            'messages',
        ];
    }

    private static function createTables(): bool
    {
        require_once ABSPATH . 'wp-admin/includes/upgrade.php';

        try {
            foreach (self::schema() as $query) {
                dbDelta($query);
            }
        } catch (\Exception $e) {
            return false;
        }

        return self::verifyTables();
    }

    /**
     * dbDelta is whitespace- and format-sensitive: two spaces after PRIMARY KEY,
     * one column definition per line, no backticks around the table name.
     *
     * @return string[]
     */
    private static function schema(): array
    {
        $charset = self::$charset_collate;
        $p = self::$wpdb->prefix . SMB2B_TABLE_PREFIX;

        $sql = [];

        $sql[] = "CREATE TABLE {$p}groups (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            name VARCHAR(191) NOT NULL,
            slug VARCHAR(191) NOT NULL,
            settings LONGTEXT NULL,
            created_at DATETIME NOT NULL,
            updated_at DATETIME NOT NULL,
            PRIMARY KEY  (id),
            UNIQUE KEY slug (slug)
        ) {$charset};";

        $sql[] = "CREATE TABLE {$p}price_rules (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            group_id BIGINT UNSIGNED NULL,
            scope VARCHAR(20) NOT NULL,
            object_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
            type VARCHAR(20) NOT NULL,
            value DECIMAL(15,4) NOT NULL,
            min_qty INT UNSIGNED NOT NULL DEFAULT 1,
            max_qty INT UNSIGNED NULL,
            priority INT NOT NULL DEFAULT 0,
            active TINYINT(1) NOT NULL DEFAULT 1,
            owner_id BIGINT UNSIGNED NULL,
            owner_type VARCHAR(20) NULL,
            created_at DATETIME NOT NULL,
            updated_at DATETIME NOT NULL,
            PRIMARY KEY  (id),
            KEY resolution (group_id, scope, object_id, min_qty),
            KEY customer_lookup (scope, object_id),
            KEY owner (owner_type, owner_id)
        ) {$charset};";

        // Added in DB 1.1.0. Order quantity constraints (FEATURES A5) are a
        // separate concern from pricing, so they get their own table rather
        // than overloading price_rules, whose index is tuned for the
        // resolution hot path.
        $sql[] = "CREATE TABLE {$p}quantity_rules (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            group_id BIGINT UNSIGNED NULL,
            scope VARCHAR(20) NOT NULL,
            object_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
            min_qty INT UNSIGNED NULL,
            max_qty INT UNSIGNED NULL,
            step_qty INT UNSIGNED NULL,
            min_order_value DECIMAL(15,4) NULL,
            active TINYINT(1) NOT NULL DEFAULT 1,
            owner_id BIGINT UNSIGNED NULL,
            owner_type VARCHAR(20) NULL,
            created_at DATETIME NOT NULL,
            updated_at DATETIME NOT NULL,
            PRIMARY KEY  (id),
            KEY lookup (group_id, scope, object_id),
            KEY object_id (object_id),
            KEY owner (owner_type, owner_id)
        ) {$charset};";

        // Added in DB 1.2.0. Buy-X-get-Y promotions (competitive gap feature).
        // Reward lives on its own line item in the cart, so this table only
        // needs to describe the trigger and the reward, not touch pricing. Free
        // creates product-scope rules; buy_type/category is recognised so Pro
        // can add category triggers and cross-product gifts. reward_scope and
        // reward_product_id (DB 1.3.0) carry Pro's cross-product and cheapest
        // rewards; Free always uses 'same' (more of the trigger product).
        $sql[] = "CREATE TABLE {$p}bogo_rules (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            title VARCHAR(191) NOT NULL DEFAULT '',
            buy_type VARCHAR(20) NOT NULL DEFAULT 'product',
            buy_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
            buy_qty INT UNSIGNED NOT NULL DEFAULT 1,
            reward_type VARCHAR(20) NOT NULL DEFAULT 'free',
            reward_qty INT UNSIGNED NOT NULL DEFAULT 1,
            reward_value DECIMAL(15,4) NOT NULL DEFAULT 0,
            reward_scope VARCHAR(20) NOT NULL DEFAULT 'same',
            reward_product_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
            group_id BIGINT UNSIGNED NULL,
            repeat_reward TINYINT(1) NOT NULL DEFAULT 1,
            active TINYINT(1) NOT NULL DEFAULT 1,
            priority INT NOT NULL DEFAULT 0,
            created_at DATETIME NOT NULL,
            updated_at DATETIME NOT NULL,
            PRIMARY KEY  (id),
            KEY lookup (active, buy_type, buy_id),
            KEY group_id (group_id)
        ) {$charset};";

        $sql[] = "CREATE TABLE {$p}companies (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            name VARCHAR(191) NOT NULL,
            vat_number VARCHAR(32) NULL,
            vat_status VARCHAR(20) NOT NULL DEFAULT 'unchecked',
            vat_checked_at DATETIME NULL,
            group_id BIGINT UNSIGNED NULL,
            billing LONGTEXT NULL,
            created_at DATETIME NOT NULL,
            updated_at DATETIME NOT NULL,
            PRIMARY KEY  (id),
            KEY group_id (group_id)
        ) {$charset};";

        $sql[] = "CREATE TABLE {$p}company_users (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            company_id BIGINT UNSIGNED NOT NULL,
            user_id BIGINT UNSIGNED NOT NULL,
            role VARCHAR(20) NOT NULL DEFAULT 'buyer',
            spend_limit_order DECIMAL(15,4) NULL,
            spend_limit_month DECIMAL(15,4) NULL,
            created_at DATETIME NOT NULL,
            PRIMARY KEY  (id),
            UNIQUE KEY company_user (company_id, user_id),
            KEY user_id (user_id)
        ) {$charset};";

        $sql[] = "CREATE TABLE {$p}invitations (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            company_id BIGINT UNSIGNED NOT NULL,
            email VARCHAR(191) NOT NULL,
            role VARCHAR(20) NOT NULL DEFAULT 'buyer',
            token_hash CHAR(64) NOT NULL,
            expires_at DATETIME NOT NULL,
            accepted_at DATETIME NULL,
            created_by BIGINT UNSIGNED NOT NULL,
            created_at DATETIME NOT NULL,
            PRIMARY KEY  (id),
            UNIQUE KEY token_hash (token_hash),
            KEY company_id (company_id)
        ) {$charset};";

        $sql[] = "CREATE TABLE {$p}quotes (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            user_id BIGINT UNSIGNED NOT NULL,
            company_id BIGINT UNSIGNED NULL,
            status VARCHAR(20) NOT NULL DEFAULT 'draft',
            expires_at DATETIME NULL,
            order_id BIGINT UNSIGNED NULL,
            conversation_id BIGINT UNSIGNED NULL,
            created_at DATETIME NOT NULL,
            updated_at DATETIME NOT NULL,
            PRIMARY KEY  (id),
            KEY user_id (user_id),
            KEY conversation_id (conversation_id),
            KEY expiry_cron (status, expires_at)
        ) {$charset};";

        $sql[] = "CREATE TABLE {$p}quote_items (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            quote_id BIGINT UNSIGNED NOT NULL,
            product_id BIGINT UNSIGNED NOT NULL,
            variation_id BIGINT UNSIGNED NULL,
            quantity INT UNSIGNED NOT NULL DEFAULT 1,
            requested_price DECIMAL(15,4) NULL,
            offered_price DECIMAL(15,4) NULL,
            PRIMARY KEY  (id),
            KEY quote_id (quote_id)
        ) {$charset};";

        $sql[] = "CREATE TABLE {$p}quote_messages (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            quote_id BIGINT UNSIGNED NOT NULL,
            user_id BIGINT UNSIGNED NOT NULL,
            message TEXT NOT NULL,
            created_at DATETIME NOT NULL,
            PRIMARY KEY  (id),
            KEY quote_id (quote_id)
        ) {$charset};";

        $sql[] = "CREATE TABLE {$p}credit_accounts (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            company_id BIGINT UNSIGNED NOT NULL,
            credit_limit DECIMAL(15,4) NOT NULL DEFAULT 0,
            terms_days INT UNSIGNED NOT NULL DEFAULT 30,
            created_at DATETIME NOT NULL,
            PRIMARY KEY  (id),
            UNIQUE KEY company_id (company_id)
        ) {$charset};";

        // Append-only ledger (FEATURES G2) — never UPDATE or DELETE rows here.
        $sql[] = "CREATE TABLE {$p}credit_transactions (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            account_id BIGINT UNSIGNED NOT NULL,
            type VARCHAR(20) NOT NULL,
            amount DECIMAL(15,4) NOT NULL,
            order_id BIGINT UNSIGNED NULL,
            refund_id BIGINT UNSIGNED NULL,
            due_date DATE NULL,
            note VARCHAR(255) NULL,
            created_by BIGINT UNSIGNED NULL,
            created_at DATETIME NOT NULL,
            PRIMARY KEY  (id),
            KEY statements (account_id, due_date),
            KEY order_id (order_id)
        ) {$charset};";

        // Added in DB 1.4.0. Saved purchase lists (FEATURES F1 reorder). Items
        // are a JSON array of {product_id, variation_id, quantity} — a list is
        // always read and written as a whole snapshot, so a child table would be
        // needless joins. Reorder re-prices live, so no price is stored here.
        // company_id (DB 1.5.0): NULL = personal; set = shared with that
        // company, visible to every member (Pro concept, core owns the schema).
        $sql[] = "CREATE TABLE {$p}purchase_lists (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            user_id BIGINT UNSIGNED NOT NULL,
            company_id BIGINT UNSIGNED NULL,
            name VARCHAR(191) NOT NULL,
            items LONGTEXT NOT NULL,
            created_at DATETIME NOT NULL,
            updated_at DATETIME NOT NULL,
            PRIMARY KEY  (id),
            KEY user_lookup (user_id),
            KEY company_lookup (company_id)
        ) {$charset};";

        // Added in DB 1.6.0. Built-in Communication & Negotiations
        // ([[project-messaging-feature]]). A conversation is a buyer<->shop
        // thread that optionally anchors to an order/product/quote (anchor_type
        // + anchor_id; empty = general). Read state is two cursors — one per
        // side — holding the id of the last message that side has seen, so
        // "unread" is a pure id comparison with no per-message flags. The
        // denormalised last_message_* columns keep the inbox list sortable and
        // the unread test cheap without touching the messages table. company_id
        // (Pro) makes the thread readable by every member of that company;
        // Free always leaves it NULL. Quote threads migrate onto these tables
        // later (one message model) — quote_messages stays until they do.
        $sql[] = "CREATE TABLE {$p}conversations (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            subject VARCHAR(191) NOT NULL DEFAULT '',
            buyer_id BIGINT UNSIGNED NOT NULL,
            company_id BIGINT UNSIGNED NULL,
            anchor_type VARCHAR(20) NOT NULL DEFAULT '',
            anchor_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
            status VARCHAR(20) NOT NULL DEFAULT 'open',
            last_message_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
            last_author_type VARCHAR(10) NOT NULL DEFAULT '',
            last_message_at DATETIME NULL,
            buyer_last_read_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
            shop_last_read_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
            created_at DATETIME NOT NULL,
            updated_at DATETIME NOT NULL,
            PRIMARY KEY  (id),
            KEY buyer_lookup (buyer_id),
            KEY company_lookup (company_id),
            KEY inbox (status, last_message_at)
        ) {$charset};";

        // author_type is 'buyer', 'shop', or 'system' (status transitions and
        // other events write a system line into the thread so the read cursors
        // cover events as well as chat — the thread doubles as an audit trail).
        $sql[] = "CREATE TABLE {$p}messages (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            conversation_id BIGINT UNSIGNED NOT NULL,
            user_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
            author_type VARCHAR(10) NOT NULL DEFAULT 'buyer',
            body TEXT NOT NULL,
            created_at DATETIME NOT NULL,
            PRIMARY KEY  (id),
            KEY thread (conversation_id, id)
        ) {$charset};";

        return $sql;
    }

    /**
     * dbDelta reports success even when a CREATE silently fails, so confirm
     * each table actually landed before we record the version.
     */
    private static function verifyTables(): bool
    {
        foreach (self::tables() as $table) {
            $name = self::tableName($table);

            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
            $found = self::$wpdb->get_var( self::$wpdb->prepare('SHOW TABLES LIKE %s', $name) );

            if ($found !== $name) {
                return false;
            }
        }

        return true;
    }

    public static function deactivate(): void
    {
        // Schema and data survive deactivation by design — only scheduled work stops.
    }

    /**
     * Fallback for the registered uninstall hook. The canonical path is
     * uninstall.php, which WordPress prefers and which stops this from firing;
     * both delegate to the same place so the behaviour cannot diverge.
     */
    public static function uninstall(): void
    {
        \SoftminalB2B\Services\Uninstaller::run();
    }
}
