<?php
/**
 * Payment Method Repository.
 *
 * All read/write operations against:
 *   - wp_tc_payment_methods           (gateway config rows)
 *   - wp_tc_payment_method_translations (per-language labels / instructions)
 *
 * Follows the same pattern as OrderRepository and CategoryRepository.
 *
 * SETTINGS STORAGE
 * ----------------
 * Gateway API keys and secrets are stored in the `settings` JSON column.
 * They are obfuscated (base64) on write and decoded on read — NOT encrypted
 * at rest (WordPress has no built-in symmetric encryption).
 * For production deployments, store sensitive keys in wp-config.php constants
 * and reference them via gateway settings field defaults; this repo just
 * persists whatever the admin submits.
 *
 * @package TitanCart\Modules\Payments
 */

namespace TitanCart\Modules\Payments;

use TitanCart\DB\Database;

defined( 'ABSPATH' ) || exit;

class PaymentMethodRepository {

    private Database $db;

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

    // ══════════════════════════════════════════════════════════════════════════
    // READ
    // ══════════════════════════════════════════════════════════════════════════

    /**
     * Get all payment methods with their translated labels for the given language.
     *
     * @param  string $lang     Language code, default 'en'.
     * @param  string $status   'active' | 'disabled' | '' (all).
     * @return array
     */
    public function get_all( string $lang = 'en', string $status = '' ): array {
        $where  = $status ? "WHERE pm.status = ?s" : '';
        $args   = $status ? [ $lang, $status ] : [ $lang ];

        $rows = $this->db->getArray(
            "SELECT pm.*,
                    COALESCE(pmt.name,         pm.gateway) AS label,
                    COALESCE(pmt.description,  '')          AS description,
                    COALESCE(pmt.instructions, '')          AS instructions
             FROM {$this->db->table('payment_methods')} pm
             LEFT JOIN {$this->db->table('payment_method_translations')} pmt
                 ON pmt.method_id = pm.id AND pmt.language_code = ?s
             {$where}
             ORDER BY pm.position ASC",
            ...$args
        );

        return array_map( [ $this, 'decode_row' ], $rows );
    }

    /**
     * Get active payment methods only (used by checkout + CheckoutManager).
     *
     * @param  string $lang
     * @return array
     */
    public function get_active( string $lang = 'en' ): array {
        return $this->get_all( $lang, 'active' );
    }

    /**
     * Get a single payment method by primary key.
     *
     * @param  int    $id
     * @param  string $lang
     * @return array|null
     */
    public function get_by_id( int $id, string $lang = 'en' ): ?array {
        $row = $this->db->getRow(
            "SELECT pm.*,
                    COALESCE(pmt.name,         pm.gateway) AS label,
                    COALESCE(pmt.description,  '')          AS description,
                    COALESCE(pmt.instructions, '')          AS instructions
             FROM {$this->db->table('payment_methods')} pm
             LEFT JOIN {$this->db->table('payment_method_translations')} pmt
                 ON pmt.method_id = pm.id AND pmt.language_code = ?s
             WHERE pm.id = ?i LIMIT 1",
            $lang,
            $id
        );

        return $row ? $this->decode_row( $row ) : null;
    }

    /**
     * Retrieve the raw decoded settings for a payment method.
     * Returns empty array if the method does not exist.
     *
     * @param  int $id
     * @return array
     */
    public function get_settings( int $id ): array {
        $raw = $this->db->getField(
            "SELECT settings FROM {$this->db->table('payment_methods')} WHERE id = ?i LIMIT 1",
            $id
        );

        if ( ! $raw ) {
            return [];
        }

        $decoded = json_decode( $raw, true );
        return is_array( $decoded ) ? $decoded : [];
    }

    // ══════════════════════════════════════════════════════════════════════════
    // WRITE
    // ══════════════════════════════════════════════════════════════════════════

    /**
     * Insert a new payment method record.
     *
     * @param  array $data  Keys: gateway, position, surcharge, surcharge_type, settings, status, test_mode.
     * @param  array $translations  [ lang_code => [ name, description, instructions ] ]
     * @return int  New method ID, 0 on failure.
     */
    public function create( array $data, array $translations = [] ): int {
        $id = $this->db->insert( 'payment_methods', [
            'gateway'        => sanitize_key( $data['gateway'] ),
            'position'       => (int) ( $data['position']       ?? 0 ),
            'surcharge'      => (float) ( $data['surcharge']      ?? 0 ),
            // ⚠️ KI-82/KI-33: the default MUST appear on BOTH sides of the ternary.
            // `in_array( $data['x'] ?? 'default', … )` tests the DEFAULT, but the
            // true-branch returned the RAW key — an undefined index on a bare payload,
            // which evaluates to NULL. Both columns are NOT NULL ENUM (Schema L1263 /
            // L1265) and an explicit NULL does NOT fall back to a column DEFAULT, so
            // MySQL rejected the row (1048) and create() returned 0 → 500.
            'surcharge_type' => in_array( $data['surcharge_type'] ?? 'A', [ 'A', 'P' ], true )
                                    ? ( $data['surcharge_type'] ?? 'A' )
                                    : 'A',
            'settings'       => wp_json_encode( $data['settings'] ?? [] ),
            'status'         => in_array( $data['status'] ?? 'active', [ 'active', 'disabled' ], true )
                                    ? ( $data['status'] ?? 'active' )
                                    : 'active',
            'test_mode'      => (int) ! empty( $data['test_mode'] ),
        ] );

        if ( $id && $translations ) {
            $this->save_translations( $id, $translations );
        }

        return (int) $id;
    }

    /**
     * Update an existing payment method.
     *
     * @param  int   $id
     * @param  array $data
     * @param  array $translations
     * @return bool
     */
    public function update( int $id, array $data, array $translations = [] ): bool {
        $fields = [];

        if ( isset( $data['position'] ) )       $fields['position']       = (int) $data['position'];
        if ( isset( $data['surcharge'] ) )      $fields['surcharge']      = (float) $data['surcharge'];
        if ( isset( $data['surcharge_type'] ) ) $fields['surcharge_type'] = in_array( $data['surcharge_type'], [ 'A', 'P' ], true ) ? $data['surcharge_type'] : 'A';
        if ( isset( $data['status'] ) )         $fields['status']         = in_array( $data['status'], [ 'active', 'disabled' ], true ) ? $data['status'] : 'active';
        if ( isset( $data['test_mode'] ) )      $fields['test_mode']      = (int) (bool) $data['test_mode'];
        if ( isset( $data['settings'] ) )       $fields['settings']       = wp_json_encode( $data['settings'] );

        $ok = empty( $fields ) ? true : ( false !== $this->db->update( 'payment_methods', $fields, [ 'id' => $id ] ) );

        if ( $translations ) {
            $this->save_translations( $id, $translations );
        }

        return $ok;
    }

    /**
     * Soft-disable: set status to 'disabled'. Retained as the mechanism behind the
     * Enable/Disable toggle's semantics; the DELETE route no longer uses it.
     *
     * @param int $id
     */
    public function disable( int $id ): void {
        $this->db->update( 'payment_methods', [ 'status' => 'disabled' ], [ 'id' => $id ] );
    }

    /**
     * Hard-delete a payment gateway, backfilling order history first.
     *
     * KI-97 (S424). This method's predecessor docblock read "Hard-delete is
     * intentionally not exposed — transactions reference this row." That concern
     * was checked rather than inherited, and it does not hold:
     * tc_payment_transactions carries its OWN `gateway VARCHAR(64) NOT NULL`
     * column (Schema.php L1292) and a nullable payment_method_id, so a deleted
     * gateway costs a transaction nothing.
     *
     * ⚠️ THE REAL HAZARD WAS ELSEWHERE, and it is what step 1 exists for: the
     * payment method NAME on a historical order was resolved ONLY by live join,
     * so deleting a gateway blanked it everywhere it was displayed. Orders placed
     * from S424 onward carry their own payment_info snapshot from checkout; orders
     * placed BEFORE that have payment_info = NULL and must be backfilled here,
     * while the row still exists to be read. Deleting first would destroy the
     * only remaining copy of the name.
     *
     * Orders that already hold a snapshot are deliberately NOT touched — theirs
     * records the name as it stood at their own checkout, which is the truth this
     * column is for.
     *
     * @param  int $id
     * @return bool  False if no such method.
     */
    public function delete( int $id ): bool {
        $method = $this->get_by_id( $id );

        if ( ! $method ) {
            return false;
        }

        // ── 1. Backfill history BEFORE the row is gone. ────────────────────────
        $snapshot = wp_json_encode( [
            'method_id' => $id,
            'gateway'   => (string) ( $method['gateway'] ?? '' ),
            'name'      => (string) ( $method['label'] ?? $method['gateway'] ?? '' ),
            'backfill'  => true,
        ] );

        $this->db->query(
            "UPDATE {$this->db->table('orders')}
                SET payment_info = ?s
              WHERE payment_method_id = ?i
                AND payment_info IS NULL",
            $snapshot,
            $id
        );

        // ── 2. Cascade + delete. ───────────────────────────────────────────────
        $this->db->query(
            "DELETE FROM {$this->db->table('payment_method_translations')} WHERE method_id = ?i",
            $id
        );

        $this->db->query(
            "DELETE FROM {$this->db->table('payment_methods')} WHERE id = ?i",
            $id
        );

        return true;
    }

    /**
     * Count what a payment gateway is referenced by. Informational only — it does
     * not gate the delete, since delete() backfills the one thing at risk.
     *
     * @param  int $id
     * @return array{orders:int, backfilled:int, transactions:int}
     */
    public function count_usage( int $id ): array {
        return [
            'orders'       => (int) $this->db->getField(
                "SELECT COUNT(*) FROM {$this->db->table('orders')} WHERE payment_method_id = ?i",
                $id
            ),
            'backfilled'   => (int) $this->db->getField(
                "SELECT COUNT(*) FROM {$this->db->table('orders')}
                  WHERE payment_method_id = ?i AND payment_info IS NULL",
                $id
            ),
            'transactions' => (int) $this->db->getField(
                "SELECT COUNT(*) FROM {$this->db->table('payment_transactions')} WHERE payment_method_id = ?i",
                $id
            ),
        ];
    }

    // ══════════════════════════════════════════════════════════════════════════
    // SURCHARGE
    // ══════════════════════════════════════════════════════════════════════════

    /**
     * Calculate the payment surcharge for a given order subtotal.
     *
     * Surcharge types:
     *   A = absolute amount (e.g. $2.50)
     *   P = percentage of subtotal (e.g. 2.5%)
     *
     * @param  int   $method_id  Payment method ID.
     * @param  float $subtotal   Cart subtotal before surcharge.
     * @return float
     */
    public function calculate_surcharge( int $method_id, float $subtotal ): float {
        $method = $this->db->getRow(
            "SELECT surcharge, surcharge_type FROM {$this->db->table('payment_methods')}
             WHERE id = ?i AND status = 'active' LIMIT 1",
            $method_id
        );

        if ( ! $method || (float) $method['surcharge'] === 0.0 ) {
            return 0.0;
        }

        if ( $method['surcharge_type'] === 'P' ) {
            return round( $subtotal * ( (float) $method['surcharge'] / 100 ), 4 );
        }

        return round( (float) $method['surcharge'], 4 );
    }

    // ══════════════════════════════════════════════════════════════════════════
    // PRIVATE HELPERS
    // ══════════════════════════════════════════════════════════════════════════

    /**
     * Upsert translations for a payment method.
     *
     * @param int   $method_id
     * @param array $translations  [ 'en' => ['name' => '...', 'description' => '...', 'instructions' => '...'] ]
     */
    private function save_translations( int $method_id, array $translations ): void {
        foreach ( $translations as $lang => $t ) {
            $existing = $this->db->getField(
                "SELECT id FROM {$this->db->table('payment_method_translations')}
                 WHERE method_id = ?i AND language_code = ?s LIMIT 1",
                $method_id,
                $lang
            );

            $row = [
                'method_id'     => $method_id,
                'language_code' => $lang,
                'name'          => sanitize_text_field( $t['name']         ?? '' ),
                'description'   => wp_kses_post( $t['description']         ?? '' ),
                'instructions'  => wp_kses_post( $t['instructions']        ?? '' ),
            ];

            if ( $existing ) {
                $this->db->update( 'payment_method_translations', $row, [ 'id' => (int) $existing ] );
            } else {
                $this->db->insert( 'payment_method_translations', $row );
            }
        }
    }

    /**
     * Decode the `settings` JSON column on a row after a SELECT.
     * Strips sensitive keys from public-facing responses via the
     * `titancart_payment_settings_public` filter when needed.
     *
     * @param  array $row
     * @return array
     */
    private function decode_row( array $row ): array {
        if ( isset( $row['settings'] ) && is_string( $row['settings'] ) ) {
            $decoded = json_decode( $row['settings'], true );
            $row['settings'] = is_array( $decoded ) ? $decoded : [];
        }
        return $row;
    }
}
