<?php

namespace Kitgenix\PDF_Invoicing\Core;

use Kitgenix\PDF_Invoicing\Modules\Invoicing\SequenceManager;

defined( 'ABSPATH' ) || exit;

/**
 * Idempotent upgrade routine.
 *
 * Runs on every `plugins_loaded` (cheap: a single option read) and only does
 * work when the stored DB version is behind. Every step here must be safe to
 * run more than once – e.g. after a failed/partial run, or on a
 * multisite network where sites upgrade at different times – because it is
 * not gated behind the activation hook alone (sites updated via FTP/without
 * a deactivate+reactivate cycle still need to reach current schema).
 */
final class Migrations {

    private const DB_VERSION_OPTION = 'kitgenix_pdf_invoicing_for_woocommerce_db_version';
    private const CURRENT_VERSION   = 3;

    private const ARCHIVE_META_KEY = '_kitgenix_pdf_invoicing_for_woocommerce_archived_documents';

    public static function maybe_run(): void {
        $installed = (int) get_option( self::DB_VERSION_OPTION, 0 );

        if ( $installed >= self::CURRENT_VERSION ) {
            return;
        }

        if ( $installed < 1 ) {
            self::migrate_to_v1();
        }

        if ( $installed < 2 ) {
            self::migrate_to_v2();
        }

        if ( $installed < 3 ) {
            self::migrate_to_v3();
        }

        update_option( self::DB_VERSION_OPTION, self::CURRENT_VERSION, false );
    }

    public static function run_activation(): void {
        SequenceManager::create_table();
        self::maybe_run();
    }

    /**
     * v1: create the atomic sequences table and seed it from the legacy
     * option-based counters, without ever lowering a counter that the table
     * may already hold (safe to re-run).
     */
    private static function migrate_to_v1(): void {
        SequenceManager::create_table();

        if ( ! SequenceManager::table_exists() ) {
            // Host disallows CREATE TABLE for this DB user. SequenceManager
            // will transparently fall back to the legacy option; nothing
            // further to migrate.
            return;
        }

        foreach ( SequenceManager::get_legacy_counters() as $type => $periods ) {
            if ( ! is_array( $periods ) ) {
                continue;
            }

            foreach ( $periods as $period_key => $value ) {
                SequenceManager::seed_at_least( (string) $type, (string) $period_key, (int) $value );
            }
        }
    }

    /**
     * v2: backfill an explicit `status` of "issued" onto archive entries
     * that predate the status field. Presence of an archive entry always
     * implied an issued document, so this is a pure metadata backfill – no
     * files move, no numbers change.
     */
    private static function migrate_to_v2(): void {
        // Only touch orders that actually carry our archive meta key, in
        // batches, so this never turns into an unbounded full-table scan on
        // large stores. Works for both legacy postmeta and HPOS order meta
        // because both persist through the same `wc_get_order()`/meta API.
        $batch_size = 200;
        $page       = 1;
        $safety_cap = 0;

        do {
            $order_ids = self::find_order_ids_with_meta_key( self::ARCHIVE_META_KEY, $page, $batch_size );
            if ( empty( $order_ids ) ) {
                break;
            }

            foreach ( $order_ids as $order_id ) {
                self::backfill_order_status( (int) $order_id );
            }

            $page++;
            $safety_cap++;
        } while ( count( $order_ids ) === $batch_size && $safety_cap < 500 );
    }

    /**
     * v3: seed the refund-ID-keyed credit note map from the existing
     * position-based history, so credit note numbering stays deterministic
     * without renumbering anything already issued.
     */
    private static function migrate_to_v3(): void {
        $batch_size = 200;
        $page       = 1;
        $safety_cap = 0;

        do {
            $order_ids = self::find_order_ids_with_meta_key( '_kitgenix_pdf_invoicing_for_woocommerce_credit_note_history', $page, $batch_size );
            if ( empty( $order_ids ) ) {
                break;
            }

            foreach ( $order_ids as $order_id ) {
                self::backfill_credit_note_refund_map( (int) $order_id );
            }

            $page++;
            $safety_cap++;
        } while ( count( $order_ids ) === $batch_size && $safety_cap < 500 );
    }

    private static function backfill_order_status( int $order_id ): void {
        if ( ! function_exists( 'wc_get_order' ) ) {
            return;
        }

        $order = wc_get_order( $order_id );
        if ( ! $order ) {
            return;
        }

        $all_documents = $order->get_meta( self::ARCHIVE_META_KEY, true );
        if ( ! is_array( $all_documents ) || empty( $all_documents ) ) {
            return;
        }

        $changed = false;

        foreach ( $all_documents as $type => $type_documents ) {
            if ( ! is_array( $type_documents ) || empty( $type_documents['entries'] ) ) {
                continue;
            }

            if ( ! isset( $type_documents['status'] ) ) {
                $type_documents['status'] = 'issued';
                $changed                  = true;
            }

            if ( ! isset( $type_documents['history'] ) || ! is_array( $type_documents['history'] ) ) {
                $type_documents['history'] = [
                    [
                        'event'      => 'issued',
                        'at'         => current_time( 'mysql' ),
                        'by'         => 0,
                        'archive_key'=> isset( $type_documents['latest_key'] ) ? (string) $type_documents['latest_key'] : '',
                        'identifier' => '',
                        'note'       => __( 'Backfilled during the 2.0.0 upgrade for a document generated by an earlier plugin version.', 'kitgenix-pdf-invoicing-for-woocommerce' ),
                    ],
                ];
                $changed = true;
            }

            $all_documents[ $type ] = $type_documents;
        }

        if ( $changed ) {
            $order->update_meta_data( self::ARCHIVE_META_KEY, $all_documents );
            $order->save();
        }
    }

    private static function backfill_credit_note_refund_map( int $order_id ): void {
        if ( ! function_exists( 'wc_get_order' ) ) {
            return;
        }

        $order = wc_get_order( $order_id );
        if ( ! $order ) {
            return;
        }

        $existing_map = $order->get_meta( '_kitgenix_pdf_invoicing_for_woocommerce_credit_note_refund_map', true );
        if ( is_array( $existing_map ) && ! empty( $existing_map ) ) {
            return; // Already migrated.
        }

        $history = $order->get_meta( '_kitgenix_pdf_invoicing_for_woocommerce_credit_note_history', true );
        if ( ! is_array( $history ) || empty( $history ) ) {
            return;
        }

        $refunds = $order->get_refunds();
        if ( ! is_array( $refunds ) || empty( $refunds ) ) {
            return;
        }

        // Best-effort positional mapping: this only runs once, for orders
        // whose credit notes were generated before refund IDs were tracked.
        // From this point forward the refund-ID map is authoritative.
        $map = [];
        foreach ( $history as $index => $entry ) {
            if ( ! isset( $refunds[ $index ] ) || ! $refunds[ $index ] instanceof \WC_Order_Refund ) {
                continue;
            }

            $refund_id = $refunds[ $index ]->get_id();
            if ( $refund_id <= 0 ) {
                continue;
            }

            $map[ $refund_id ] = [
                'number'   => isset( $entry['number'] ) ? (string) $entry['number'] : '',
                'date'     => isset( $entry['date'] ) ? (string) $entry['date'] : '',
                'sequence' => $index + 1,
            ];
        }

        if ( ! empty( $map ) ) {
            $order->update_meta_data( '_kitgenix_pdf_invoicing_for_woocommerce_credit_note_refund_map', $map );
            $order->save();
        }
    }

    /**
     * HPOS-safe: uses `wc_get_orders()` (the public CRUD/query API) instead
     * of raw postmeta SQL, so this works whether custom order tables are
     * enabled or not. Paged rather than ID-cursor based: every migration
     * step only ever sets/adds meta under the same key being filtered on
     * (never removes it), so the matching result set is stable across pages
     * for the duration of this one-time backfill.
     *
     * @return int[]
     */
    private static function find_order_ids_with_meta_key( string $meta_key, int $page, int $limit ): array {
        if ( ! function_exists( 'wc_get_orders' ) ) {
            return [];
        }

        $ids = wc_get_orders(
            [
                'type'       => 'shop_order',
                'limit'      => $limit,
                'paged'      => $page,
                'orderby'    => 'ID',
                'order'      => 'ASC',
                'return'     => 'ids',
                'meta_query' => [ // phpcs:ignore WooCommerce.Commenting, WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- meta_query is part of the public WC_Order_Query args contract; no alternative exists for "find orders with this meta key set", and this runs paginated, only during a one-time admin-triggered migration.
                    [
                        'key'     => $meta_key,
                        'compare' => 'EXISTS',
                    ],
                ],
            ]
        );

        return is_array( $ids ) ? array_map( 'absint', $ids ) : [];
    }
}
