<?php
/**
 * ShippingZone — Zone data access and address-matching.
 *
 * Responsibilities:
 *   - CRUD for tc_shipping_zones and tc_shipping_zone_locations
 *   - Address matching: given a shipping address, return the zone IDs
 *     (ordered most-specific first) that apply to that address
 *
 * Zone-matching hierarchy (most specific wins):
 *   postcode match > state match > country match > worldwide (no locations)
 *
 * Postcode patterns supported:
 *   - Exact:    "90210"
 *   - Wildcard: "902*"     (prefix followed by asterisk)
 *   - Range:    "90000...90999"
 *
 * State codes are stored as "COUNTRY:STATE" (e.g. "US:CA") to avoid
 * ambiguity between regions in different countries.
 *
 * @package TitanCart\Modules\Shipping
 */

namespace TitanCart\Modules\Shipping;

use TitanCart\DB\Database;

defined( 'ABSPATH' ) || exit;

class ShippingZone {

    private Database $db;

    public function __construct() {
        $this->db = titancart_db();
    }

    // ══════════════════════════════════════════════════════════════════════════
    // ZONE CRUD
    // ══════════════════════════════════════════════════════════════════════════

    /**
     * Return all zones with their location rows attached.
     *
     * @return array[]
     */
    public function get_all(): array {
        $zones = $this->db->getArray(
            "SELECT * FROM {$this->db->table('shipping_zones')}
             ORDER BY position ASC, id ASC"
        );

        foreach ( $zones as &$zone ) {
            $zone['locations'] = $this->get_locations( (int) $zone['id'] );
        }

        return $zones;
    }

    /**
     * Return a single zone with locations, or null if not found.
     *
     * @param  int $id
     * @return array|null
     */
    public function get_by_id( int $id ): ?array {
        $zone = $this->db->getRow(
            "SELECT * FROM {$this->db->table('shipping_zones')} WHERE id = ?i LIMIT 1",
            $id
        );

        if ( ! $zone ) {
            return null;
        }

        $zone['locations'] = $this->get_locations( $id );
        return $zone;
    }

    /**
     * Create a new zone and return its ID.
     *
     * @param  array $data  {name, position?, status?}
     * @return int
     */
    public function create( array $data ): int {
        $this->db->query(
            "INSERT INTO {$this->db->table('shipping_zones')} (name, position, status)
             VALUES (?s, ?i, ?s)",
            sanitize_text_field( $data['name']     ?? '' ),
            (int) ( $data['position'] ?? 0 ),
            // KI-84: default on BOTH sides — the `in_array` tests the default, so an
            // absent/NULL key reaches the true-branch and must re-apply the `??`.
            in_array( $data['status'] ?? 'active', [ 'active', 'disabled' ], true ) ? ( $data['status'] ?? 'active' ) : 'active'
        );

        return $this->db->lastInsertId();
    }

    /**
     * Update zone fields.
     *
     * @param  int   $id
     * @param  array $data  Subset of {name, position, status}
     * @return bool
     */
    public function update( int $id, array $data ): bool {
        $allowed = [ 'name', 'position', 'status' ];
        $fields  = array_intersect_key( $data, array_flip( $allowed ) );

        if ( empty( $fields ) ) {
            return false;
        }

        if ( isset( $fields['name'] ) ) {
            $fields['name'] = sanitize_text_field( $fields['name'] );
        }

        $this->db->query(
            "UPDATE {$this->db->table('shipping_zones')} SET ?u WHERE id = ?i",
            $fields,
            $id
        );

        return true;
    }

    /**
     * Delete a zone and cascade to its locations and rates.
     *
     * @param  int $id
     * @return bool
     */
    public function delete( int $id ): bool {
        // Remove locations.
        $this->db->query(
            "DELETE FROM {$this->db->table('shipping_zone_locations')} WHERE zone_id = ?i",
            $id
        );

        // Remove rate conditions for rates in this zone.
        $rate_ids = $this->db->getColumn(
            "SELECT id FROM {$this->db->table('shipping_rates')} WHERE zone_id = ?i",
            $id
        );

        if ( ! empty( $rate_ids ) ) {
            $this->db->query(
                "DELETE FROM {$this->db->table('shipping_rate_conditions')} WHERE rate_id IN (?a)",
                $rate_ids
            );
        }

        // Remove rate rows.
        $this->db->query(
            "DELETE FROM {$this->db->table('shipping_rates')} WHERE zone_id = ?i",
            $id
        );

        // Remove zone.
        $this->db->query(
            "DELETE FROM {$this->db->table('shipping_zones')} WHERE id = ?i",
            $id
        );

        return true;
    }

    // ══════════════════════════════════════════════════════════════════════════
    // LOCATION CRUD
    // ══════════════════════════════════════════════════════════════════════════

    /**
     * Return all location rows for a zone.
     *
     * @param  int $zone_id
     * @return array[]
     */
    public function get_locations( int $zone_id ): array {
        return $this->db->getArray(
            "SELECT * FROM {$this->db->table('shipping_zone_locations')}
             WHERE zone_id = ?i ORDER BY type ASC, code ASC",
            $zone_id
        );
    }

    /**
     * Add a destination to a zone.
     *
     * @param  int    $zone_id
     * @param  string $type   'country' | 'state' | 'postcode'
     * @param  string $code   ISO2 country, "COUNTRY:STATE" for states, or postcode/pattern.
     * @return int   New location ID.
     */
    public function add_location( int $zone_id, string $type, string $code ): int {
        if ( ! in_array( $type, [ 'country', 'state', 'postcode' ], true ) ) {
            $type = 'country';
        }

        $code = strtoupper( trim( $code ) );

        $this->db->query(
            "INSERT INTO {$this->db->table('shipping_zone_locations')} (zone_id, type, code)
             VALUES (?i, ?s, ?s)",
            $zone_id,
            $type,
            $code
        );

        return $this->db->lastInsertId();
    }

    /**
     * Remove a single location row.
     *
     * @param  int $location_id
     * @return bool
     */
    public function remove_location( int $location_id ): bool {
        $this->db->query(
            "DELETE FROM {$this->db->table('shipping_zone_locations')} WHERE id = ?i",
            $location_id
        );

        return true;
    }

    // ══════════════════════════════════════════════════════════════════════════
    // ADDRESS MATCHING
    // ══════════════════════════════════════════════════════════════════════════

    /**
     * Return the IDs of all active zones that match the given shipping address,
     * ordered from most-specific to least-specific.
     *
     * Specificity:
     *   1. Postcode-level zones (most specific)
     *   2. State-level zones
     *   3. Country-level zones
     *   4. Worldwide zones — those with NO locations (catch-all fallback)
     *
     * @param  array $address  {country (ISO2), state (state/province code), postcode}
     * @return int[]
     */
    public function get_matching_zone_ids( array $address ): array {
        $country  = strtoupper( trim( $address['country']  ?? '' ) );
        $state    = strtoupper( trim( $address['state']    ?? '' ) );
        $postcode = strtoupper( trim( $address['postcode'] ?? $address['zip'] ?? '' ) );

        // State is stored as "COUNTRY:STATE" for unambiguous matching.
        $state_key = ( $country && $state ) ? "{$country}:{$state}" : '';

        // Fetch all active zones with their locations in a single JOIN.
        $rows = $this->db->getArray(
            "SELECT z.id AS zone_id,
                    z.position,
                    l.id AS loc_id,
                    l.type,
                    l.code
             FROM {$this->db->table('shipping_zones')} z
             LEFT JOIN {$this->db->table('shipping_zone_locations')} l ON l.zone_id = z.id
             WHERE z.status = 'active'
             ORDER BY z.position ASC, z.id ASC"
        );

        // Group into a map: zone_id → {position, locations[]}
        $zones_map = [];
        foreach ( $rows as $row ) {
            $zid = (int) $row['zone_id'];
            if ( ! isset( $zones_map[ $zid ] ) ) {
                $zones_map[ $zid ] = [
                    'position'  => (int) $row['position'],
                    'locations' => [],
                ];
            }
            if ( $row['loc_id'] ) {
                $zones_map[ $zid ]['locations'][] = [
                    'type' => $row['type'],
                    'code' => $row['code'],
                ];
            }
        }

        $by_postcode = [];
        $by_state    = [];
        $by_country  = [];
        $worldwide   = [];

        foreach ( $zones_map as $zid => $zone ) {
            $locs = $zone['locations'];

            // Worldwide zone — no destinations configured.
            if ( empty( $locs ) ) {
                $worldwide[] = $zid;
                continue;
            }

            $has_postcode = false;
            $has_state    = false;
            $has_country  = false;

            foreach ( $locs as $loc ) {
                switch ( $loc['type'] ) {
                    case 'country':
                        if ( $loc['code'] === $country ) {
                            $has_country = true;
                        }
                        break;

                    case 'state':
                        $code_up = strtoupper( $loc['code'] );
                        if ( ( $state && $code_up === $state ) || ( $state_key && $code_up === $state_key ) ) {
                            $has_state = true;
                        }
                        break;

                    case 'postcode':
                        if ( $postcode && $this->postcode_matches( $loc['code'], $postcode ) ) {
                            $has_postcode = true;
                        }
                        break;
                }
            }

            if ( $has_postcode ) {
                $by_postcode[] = $zid;
            } elseif ( $has_state ) {
                $by_state[] = $zid;
            } elseif ( $has_country ) {
                $by_country[] = $zid;
            }
            // Zone has locations but none matched — skip it.
        }

        return array_merge( $by_postcode, $by_state, $by_country, $worldwide );
    }

    // ══════════════════════════════════════════════════════════════════════════
    // INTERNAL HELPERS
    // ══════════════════════════════════════════════════════════════════════════

    /**
     * Match a postcode against a stored pattern.
     *
     * Supported patterns:
     *   - Exact:    "90210"
     *   - Wildcard: "902*"     (prefix + asterisk)
     *   - Range:    "90000...90999"
     *
     * All comparisons are case-insensitive (both sides uppercased before call).
     *
     * @param  string $pattern  Stored pattern from the DB.
     * @param  string $postcode Actual postcode from the order address (already uppercased).
     * @return bool
     */
    private function postcode_matches( string $pattern, string $postcode ): bool {
        $pattern = strtoupper( trim( $pattern ) );

        // Range: "FROM...TO"
        if ( strpos( $pattern, '...' ) !== false ) {
            [ $from, $to ] = array_map( 'trim', explode( '...', $pattern, 2 ) );
            return strcmp( $postcode, $from ) >= 0 && strcmp( $postcode, $to ) <= 0;
        }

        // Wildcard: ends with '*'
        if ( substr( $pattern, -1 ) === '*' ) {
            $prefix = substr( $pattern, 0, -1 );
            return $prefix === '' || strncmp( $postcode, $prefix, strlen( $prefix ) ) === 0;
        }

        // Exact match.
        return $postcode === $pattern;
    }
}
