<?php
/**
 * Refund Service — thin wrapper around wc_create_refund() with validation
 * tied to a return request. Used by REST /admin/refund.
 *
 * @package WPNest_Returns_For_WooCommerce
 */

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

class WPNest_RFW_Refund_Service {

    const IDEMPOTENCY_TTL = 60;

    /**
     * Issue a refund tied to a return request.
     *
     * @param array $payload {
     *     request_id      (int, required),
     *     amount          (float, optional — defaults to sum of line_items totals),
     *     reason          (string, optional),
     *     restock         (bool, default false),
     *     line_items      (array, optional) — [{ order_item_id, qty, refund_total }, …],
     *     refund_payment  (bool, default false) — when true, ask the gateway to perform the technical refund.
     * }
     * @return array|WP_Error {
     *     refund_id, amount, currency, formatted_amount, gateway_handled, edit_url
     * } or WP_Error on validation failure.
     */
    public static function issue( array $payload ) {
        if ( ! current_user_can( 'manage_woocommerce' ) ) {
            return new WP_Error( 'forbidden', __( 'You do not have permission to issue refunds.', 'wpnest-returns-for-woocommerce' ), array( 'status' => 403 ) );
        }

        $request_id = isset( $payload['request_id'] ) ? (int) $payload['request_id'] : 0;
        $request    = $request_id > 0 ? WPNest_RFW_Request_Model::get( $request_id ) : null;
        if ( ! $request ) {
            return new WP_Error( 'request_not_found', __( 'Return request not found.', 'wpnest-returns-for-woocommerce' ), array( 'status' => 404 ) );
        }

        $order = wc_get_order( (int) $request->order_id );
        if ( ! $order instanceof WC_Order ) {
            return new WP_Error( 'order_not_found', __( 'The order linked to this request no longer exists.', 'wpnest-returns-for-woocommerce' ), array( 'status' => 404 ) );
        }

        $remaining = self::remaining_refundable( $order );
        if ( $remaining <= 0 ) {
            return new WP_Error( 'already_refunded', __( 'This order has been fully refunded already.', 'wpnest-returns-for-woocommerce' ), array( 'status' => 400 ) );
        }

        $line_items_in = isset( $payload['line_items'] ) && is_array( $payload['line_items'] ) ? $payload['line_items'] : array();
        $line_items    = self::validate_line_items( $order, $line_items_in );
        if ( is_wp_error( $line_items ) ) {
            return $line_items;
        }

        $amount = isset( $payload['amount'] ) ? (float) wc_format_decimal( $payload['amount'] ) : self::sum_line_items_total( $line_items );
        if ( $amount <= 0 ) {
            return new WP_Error( 'invalid_amount', __( 'Refund amount must be greater than zero.', 'wpnest-returns-for-woocommerce' ), array( 'status' => 400 ) );
        }
        if ( $amount > $remaining + 0.0001 ) {
            return new WP_Error(
                'amount_too_high',
                sprintf(
                    /* translators: %s: formatted maximum refundable amount */
                    __( 'Refund amount exceeds the refundable balance (max %s).', 'wpnest-returns-for-woocommerce' ),
                    wp_strip_all_tags( wc_price( $remaining, array( 'currency' => $order->get_currency() ) ) )
                ),
                array( 'status' => 400 )
            );
        }

        // Idempotency — silently return the most recent matching refund within the dedup window.
        $idem_key = self::idempotency_key( $request_id, $amount, $line_items );
        $cached   = get_transient( $idem_key );
        if ( $cached && isset( $cached['refund_id'] ) ) {
            return $cached;
        }

        $reason = isset( $payload['reason'] ) ? sanitize_text_field( (string) $payload['reason'] ) : '';
        if ( '' === $reason ) {
            $reason = sprintf(
                /* translators: %d: return request ID */
                __( 'Refund issued from Return Request #%d.', 'wpnest-returns-for-woocommerce' ),
                $request_id
            );
        }
        $restock        = ! empty( $payload['restock'] );
        $refund_payment = ! empty( $payload['refund_payment'] );

        $refund = wc_create_refund(
            array(
                'amount'         => $amount,
                'reason'         => $reason,
                'order_id'       => (int) $order->get_id(),
                'line_items'     => self::format_line_items_for_wc( $line_items ),
                'restock_items'  => $restock,
                'refund_payment' => $refund_payment,
            )
        );

        if ( is_wp_error( $refund ) ) {
            return $refund;
        }
        if ( ! $refund instanceof WC_Order_Refund ) {
            return new WP_Error( 'refund_failed', __( 'Could not create the refund. Please retry.', 'wpnest-returns-for-woocommerce' ), array( 'status' => 500 ) );
        }

        $user = wp_get_current_user();
        $order->add_order_note(
            sprintf(
                /* translators: 1: formatted refund amount, 2: admin display name, 3: return request ID */
                __( 'Refund of %1$s issued from Return Requests by %2$s (request #%3$d).', 'wpnest-returns-for-woocommerce' ),
                wp_strip_all_tags( wc_price( $amount, array( 'currency' => $order->get_currency() ) ) ),
                $user && $user->exists() ? $user->display_name : 'system',
                $request_id
            ),
            0
        );

        $result = array(
            'refund_id'        => (int) $refund->get_id(),
            'amount'           => $amount,
            'currency'         => $order->get_currency(),
            'formatted_amount' => wp_strip_all_tags( wc_price( $amount, array( 'currency' => $order->get_currency() ) ) ),
            'gateway_handled'  => $refund_payment,
            'edit_url'         => method_exists( $refund, 'get_view_order_url' ) ? '' : '',
            'created_at'       => $refund->get_date_created() ? $refund->get_date_created()->date( 'Y-m-d H:i:s' ) : current_time( 'mysql' ),
        );

        set_transient( $idem_key, $result, self::IDEMPOTENCY_TTL );

        do_action( 'wpnest_rfw_refund_issued', $request_id, $refund, $order );

        return $result;
    }

    /**
     * Returns the remaining refundable amount for an order
     * (order total minus the sum of existing refunds).
     */
    public static function remaining_refundable( WC_Order $order ) {
        $total = (float) $order->get_total();
        $done  = (float) $order->get_total_refunded();
        return max( 0.0, round( $total - $done, wc_get_price_decimals() ) );
    }

    /**
     * Returns the remaining refundable quantity for a given order item id.
     */
    public static function remaining_qty( WC_Order $order, $order_item_id ) {
        $item = $order->get_item( (int) $order_item_id );
        if ( ! $item instanceof WC_Order_Item_Product ) {
            return 0;
        }
        $ordered  = (int) $item->get_quantity();
        $refunded = (int) abs( $order->get_qty_refunded_for_item( (int) $order_item_id ) );
        return max( 0, $ordered - $refunded );
    }

    /**
     * Returns the remaining refundable subtotal for a given order item id.
     */
    public static function remaining_total( WC_Order $order, $order_item_id ) {
        $item = $order->get_item( (int) $order_item_id );
        if ( ! $item instanceof WC_Order_Item_Product ) {
            return 0.0;
        }
        $line_total = (float) $item->get_total() + (float) $item->get_total_tax();
        $refunded   = (float) abs( $order->get_total_refunded_for_item( (int) $order_item_id ) );
        return max( 0.0, round( $line_total - $refunded, wc_get_price_decimals() ) );
    }

    /**
     * Validate incoming line_items against the order. Returns normalized array
     * keyed by order_item_id or WP_Error.
     */
    private static function validate_line_items( WC_Order $order, array $line_items ) {
        $out = array();
        foreach ( $line_items as $li ) {
            if ( ! is_array( $li ) ) {
                continue;
            }
            $oi_id = isset( $li['order_item_id'] ) ? (int) $li['order_item_id'] : 0;
            if ( $oi_id <= 0 ) {
                continue;
            }
            $item = $order->get_item( $oi_id );
            if ( ! $item instanceof WC_Order_Item_Product ) {
                return new WP_Error(
                    'invalid_line_item',
                    sprintf(
                        /* translators: %d: order item ID */
                        __( 'Line item #%d does not belong to this order.', 'wpnest-returns-for-woocommerce' ),
                        $oi_id
                    ),
                    array( 'status' => 400 )
                );
            }
            $qty       = isset( $li['qty'] ) ? max( 0, (int) $li['qty'] ) : 0;
            $remaining = self::remaining_qty( $order, $oi_id );
            if ( $qty > $remaining ) {
                return new WP_Error(
                    'qty_exceeds_remaining',
                    sprintf(
                        /* translators: 1: product name, 2: remaining refundable quantity */
                        __( 'Quantity for "%1$s" exceeds the remaining refundable amount (max %2$d).', 'wpnest-returns-for-woocommerce' ),
                        $item->get_name(),
                        (int) $remaining
                    ),
                    array( 'status' => 400 )
                );
            }

            $refund_total_in = isset( $li['refund_total'] ) ? (float) wc_format_decimal( $li['refund_total'] ) : 0.0;
            $remaining_total = self::remaining_total( $order, $oi_id );
            if ( $refund_total_in > $remaining_total + 0.0001 ) {
                return new WP_Error(
                    'amount_exceeds_line_remaining',
                    sprintf(
                        /* translators: 1: product name, 2: formatted maximum amount */
                        __( 'Refund amount for "%1$s" exceeds its remaining balance (max %2$s).', 'wpnest-returns-for-woocommerce' ),
                        $item->get_name(),
                        wp_strip_all_tags( wc_price( $remaining_total, array( 'currency' => $order->get_currency() ) ) )
                    ),
                    array( 'status' => 400 )
                );
            }

            $out[ $oi_id ] = array(
                'order_item_id' => $oi_id,
                'qty'           => $qty,
                'refund_total'  => $refund_total_in,
                'refund_tax'    => array(), // WC tax breakdown handled by core when refund_total covers gross.
            );
        }
        return $out;
    }

    private static function sum_line_items_total( array $line_items ) {
        $sum = 0.0;
        foreach ( $line_items as $li ) {
            $sum += (float) $li['refund_total'];
        }
        return round( $sum, wc_get_price_decimals() );
    }

    /**
     * Transform our normalized line items into the shape `wc_create_refund` expects.
     */
    private static function format_line_items_for_wc( array $line_items ) {
        $out = array();
        foreach ( $line_items as $oi_id => $li ) {
            $out[ $oi_id ] = array(
                'qty'          => (int) $li['qty'],
                'refund_total' => (float) $li['refund_total'],
                'refund_tax'   => $li['refund_tax'],
            );
        }
        return $out;
    }

    private static function idempotency_key( $request_id, $amount, array $line_items ) {
        $sig_parts = array( (int) $request_id, number_format( (float) $amount, 4, '.', '' ) );
        ksort( $line_items );
        foreach ( $line_items as $oi_id => $li ) {
            $sig_parts[] = sprintf( '%d:%d:%s', (int) $oi_id, (int) $li['qty'], number_format( (float) $li['refund_total'], 4, '.', '' ) );
        }
        return 'wpnest_rfw_refund_' . substr( hash( 'sha256', implode( '|', $sig_parts ) ), 0, 32 );
    }
}
