<?php
/**
 * Categories REST API Controller.
 *
 * Endpoints:
 *   GET    /titancart/v1/categories          List categories
 *   POST   /titancart/v1/categories          Create category
 *   GET    /titancart/v1/categories/tree      Full nested tree
 *   POST   /titancart/v1/categories/reorder  Bulk-reorder siblings under one parent
 *   GET    /titancart/v1/categories/{id}     Get category
 *   PUT    /titancart/v1/categories/{id}     Update category
 *   DELETE /titancart/v1/categories/{id}     Delete category
 *
 * @package TitanCart\REST\Products
 */

namespace TitanCart\REST\Products;

use TitanCart\Modules\Products\CategoryRepository;
use TitanCart\Modules\ImportExport\ExportService;
use TitanCart\Core\LogService;
use WP_REST_Controller;
use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;

defined( 'ABSPATH' ) || exit;

class CategoriesController extends WP_REST_Controller {

    protected $namespace = 'titancart/v1';
    protected $rest_base = 'categories';

    private CategoryRepository $repo;

    public function __construct() {
        $this->repo = new CategoryRepository();
    }

    public function register_routes(): void {
        // List + create.
        register_rest_route( $this->namespace, '/' . $this->rest_base, [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'get_items' ],
                'permission_callback' => [ $this, 'get_items_permissions_check' ],
                'args'                => $this->get_collection_params(),
            ],
            [
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => [ $this, 'create_item' ],
                'permission_callback' => [ $this, 'create_item_permissions_check' ],
            ],
        ] );

        // Tree endpoint (must be registered before /{id}).
        register_rest_route( $this->namespace, '/' . $this->rest_base . '/tree', [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'get_tree' ],
                'permission_callback' => [ $this, 'get_items_permissions_check' ],
            ],
        ] );

        // Export endpoint (must be registered before /{id}).
        register_rest_route( $this->namespace, '/' . $this->rest_base . '/export', [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'export_csv' ],
                'permission_callback' => [ $this, 'get_items_permissions_check' ],
            ],
        ] );

        // Reorder endpoint (must be registered before /{id}).
        // POST /categories/reorder  body: { parent_id, ordered_ids: [id,id,...] }
        register_rest_route( $this->namespace, '/' . $this->rest_base . '/reorder', [
            [
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => [ $this, 'reorder_items' ],
                'permission_callback' => [ $this, 'update_item_permissions_check' ],
                'args'                => [
                    'parent_id'   => [ 'type' => 'integer', 'required' => true ],
                    'ordered_ids' => [ 'type' => 'array',   'required' => true ],
                ],
            ],
        ] );

        // Single item.
        register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', [
            'args' => [ 'id' => [ 'type' => 'integer' ] ],
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'get_item' ],
                'permission_callback' => [ $this, 'get_item_permissions_check' ],
            ],
            [
                'methods'             => WP_REST_Server::EDITABLE,
                'callback'            => [ $this, 'update_item' ],
                'permission_callback' => [ $this, 'update_item_permissions_check' ],
            ],
            [
                'methods'             => WP_REST_Server::DELETABLE,
                'callback'            => [ $this, 'delete_item' ],
                'permission_callback' => [ $this, 'delete_item_permissions_check' ],
            ],
        ] );
    }

    // ── Handlers ──────────────────────────────────────────────────────────────

    /**
     * GET /categories/export
     *
     * Modes:
     *   categories              — export selected categories as CSV
     *   categories_with_subs    — export selected categories + all descendants
     *   products                — export products in selected categories
     *   products_with_subs      — export products in selected categories + descendants
     *
     * Query params:
     *   ids  — comma-separated category IDs (required)
     *   mode — one of the above (default: categories)
     */
    public function export_csv( $request ): WP_REST_Response {
        $ids_raw = sanitize_text_field( $request->get_param( 'ids' ) ?? '' );
        $mode    = sanitize_key( $request->get_param( 'mode' ) ?? 'categories' );

        if ( empty( $ids_raw ) ) {
            return new WP_REST_Response( [ 'error' => 'No category IDs provided. Select categories first.' ], 400 );
        }

        $ids = array_filter( array_map( 'intval', explode( ',', $ids_raw ) ) );
        if ( empty( $ids ) ) {
            return new WP_REST_Response( [ 'error' => 'Invalid category IDs.' ], 400 );
        }

        // For "with_subs" modes, expand IDs to include all descendants.
        $target_ids = $ids;
        if ( in_array( $mode, [ 'categories_with_subs', 'products_with_subs' ], true ) ) {
            $target_ids = $this->expand_with_descendants( $ids );
        }

        try {
            $export  = new ExportService();
            $is_product_export = in_array( $mode, [ 'products', 'products_with_subs' ], true );

            if ( $is_product_export ) {
                $result = $export->generate( 'products', [ 'category_ids' => $target_ids ], [] );
            } else {
                $result = $export->generate( 'categories', [ 'category_ids' => $target_ids ], [] );
            }

            $label = str_replace( '_', ' ', $mode );
            LogService::info( 'general', 'create', "Categories export ({$label}, " . ( $result['rows'] ?? 0 ) . ' rows)', [
                'object_type' => 'export',
                'type'        => 'categories',
                'mode'        => $mode,
                'rows'        => $result['rows'] ?? 0,
            ] );

            return new WP_REST_Response( $result, 200 );
        } catch ( \Throwable $e ) {
            return new WP_REST_Response( [ 'error' => $e->getMessage() ], 422 );
        }
    }

    /**
     * Recursively get all descendant category IDs.
     */
    private function expand_with_descendants( array $parent_ids ): array {
        global $wpdb;
        $p       = $wpdb->prefix . TITANCART_TABLE_PREFIX;
        $all_ids = $parent_ids;
        $search  = $parent_ids;

        // Walk down the tree level by level (max 10 levels deep).
        for ( $i = 0; $i < 10 && ! empty( $search ); $i++ ) {
            $placeholders = implode( ',', array_fill( 0, count( $search ), '%d' ) );
            $sql = "SELECT id FROM {$p}categories WHERE parent_id IN ({$placeholders}) AND deleted_at IS NULL";
            // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.PreparedSQL.NotPrepared -- $placeholders is a %d list built from count($search) (non-empty, loop-guarded); values passed to prepare().
            $children = $wpdb->get_col( $wpdb->prepare( $sql, ...array_map( 'intval', $search ) ) );
            $children = array_map( 'intval', $children );
            $new      = array_diff( $children, $all_ids );
            if ( empty( $new ) ) {
                break;
            }
            $all_ids = array_merge( $all_ids, $new );
            $search  = $new;
        }

        return array_unique( $all_ids );
    }

    /** GET /categories */
    public function get_items( $request ): WP_REST_Response|WP_Error {
        $lang   = sanitize_text_field( $request->get_param( 'lang' ) ?? 'en' );
        $params = [
            'status'         => sanitize_text_field( $request->get_param( 'status' ) ?? '' ),
            'parent_id'      => $request->get_param( 'parent_id' ) !== null
                ? (int) $request->get_param( 'parent_id' )
                : null,
            // S274: forward a comma-separated `ids` filter (repo already supports `ids IN`).
            // Lets the promotion-condition name-picker resolve existing ids → names in one call.
            'ids'            => array_values( array_filter( array_map( 'intval', explode( ',', (string) ( $request->get_param( 'ids' ) ?? '' ) ) ) ) ),
            'search'         => sanitize_text_field( $request->get_param( 'search' ) ?? '' ),
            'sort_by'        => sanitize_text_field( $request->get_param( 'sort_by' ) ?? 'position' ),
            'sort_order'     => sanitize_text_field( $request->get_param( 'sort_order' ) ?? 'asc' ),
            'items_per_page' => (int) ( $request->get_param( 'per_page' ) ?? 0 ),
            'page'           => (int) ( $request->get_param( 'page' ) ?? 1 ),
        ];

        $result   = $this->repo->get_list( $params, $lang );
        $response = new WP_REST_Response( $this->enrich_images_list( $result['categories'] ) );
        $response->header( 'X-TC-Total',       $result['params']['total_items'] );
        $response->header( 'X-TC-Total-Pages', $result['params']['total_pages'] );

        return $response;
    }

    /** GET /categories/tree */
    public function get_tree( WP_REST_Request $request ): WP_REST_Response {
        $lang   = sanitize_text_field( $request->get_param( 'lang' ) ?? 'en' );
        $status = sanitize_text_field( $request->get_param( 'status' ) ?? '' );
        $tree   = $this->repo->get_tree( $lang, $status );

        return new WP_REST_Response( $this->enrich_images_tree( $tree ) );
    }

    /** GET /categories/{id} */
    public function get_item( $request ): WP_REST_Response|WP_Error {
        $lang     = sanitize_text_field( $request->get_param( 'lang' ) ?? 'en' );
        $category = $this->repo->get( (int) $request->get_param( 'id' ), $lang );

        if ( ! $category ) {
            return new WP_Error( 'tc_category_not_found', __( 'Category not found.', 'titancart-lite' ), [ 'status' => 404 ] );
        }

        return new WP_REST_Response( $this->enrich_image( $category ) );
    }

    /** POST /categories */
    public function create_item( $request ): WP_REST_Response|WP_Error {
        $lang = sanitize_text_field( $request->get_param( 'lang' ) ?? 'en' );
        $data = $this->sanitize_category_input( $request );
        $id   = $this->repo->save( $data, 0, $lang );

        if ( ! $id ) {
            return new WP_Error( 'tc_category_create_failed', __( 'Could not create category.', 'titancart-lite' ), [ 'status' => 500 ] );
        }

        LogService::info( 'categories', 'create', 'Category created: #' . $id, [
            'object_type'     => 'category',
            'object_id'       => $id,
            'category_name'   => sanitize_text_field( $request->get_param( 'name' ) ?? '' ),
            'parent_id'       => (int) ( $request->get_param( 'parent_id' ) ?? 0 ),
        ] );

        $response = new WP_REST_Response( $this->enrich_image( $this->repo->get( $id, $lang ) ), 201 );
        $response->header( 'Location', rest_url( "{$this->namespace}/{$this->rest_base}/{$id}" ) );

        return $response;
    }

    /** PUT /categories/{id} */
    public function update_item( $request ): WP_REST_Response|WP_Error {
        $id   = (int) $request->get_param( 'id' );
        $lang = sanitize_text_field( $request->get_param( 'lang' ) ?? 'en' );

        if ( ! $this->repo->get( $id, $lang ) ) {
            return new WP_Error( 'tc_category_not_found', __( 'Category not found.', 'titancart-lite' ), [ 'status' => 404 ] );
        }

        $data = $this->sanitize_category_input( $request );
        $this->repo->save( $data, $id, $lang );

        LogService::info( 'categories', 'update', 'Category updated: #' . $id, [
            'object_type'     => 'category',
            'object_id'       => $id,
            'category_name'   => sanitize_text_field( $request->get_param( 'name' ) ?? '' ),
            'fields_changed'  => array_keys( array_filter( $request->get_params(), function( $v, $k ) {
                return $v !== null && ! in_array( $k, [ 'id', 'lang', '_wpnonce', '_locale' ], true );
            }, ARRAY_FILTER_USE_BOTH ) ),
        ] );

        return new WP_REST_Response( $this->enrich_image( $this->repo->get( $id, $lang ) ) );
    }

    /** DELETE /categories/{id} */
    public function delete_item( $request ): WP_REST_Response|WP_Error {
        $id      = (int) $request->get_param( 'id' );
        $recurse = (bool) ( $request->get_param( 'recurse' ) ?? true );
        $force   = (bool) $request->get_param( 'force' );
        $cat     = $this->repo->get( $id );

        if ( ! $cat ) {
            return new WP_Error( 'tc_category_not_found', __( 'Category not found.', 'titancart-lite' ), [ 'status' => 404 ] );
        }

        $db  = titancart_db();
        $pc  = $db->table( 'product_categories' );
        $pr  = $db->table( 'products' );

        // ── Guard: check for linked products ─────────────────────────────────
        //
        // Filter for p.deleted_at IS NULL — without this, orphaned link rows
        // pointing to soft-deleted (or hard-deleted) products falsely block
        // deletion. Mirrors the export-count fix (S210 ExportService).
        $product_count = (int) $db->getField(
            "SELECT COUNT(DISTINCT pc.product_id)
               FROM {$pc} pc
               JOIN {$pr} p ON p.id = pc.product_id
              WHERE pc.category_id = ?i AND p.deleted_at IS NULL",
            $id
        );

        // If recursive, also count products in child categories.
        if ( $recurse && $product_count === 0 ) {
            $cat_table = $db->table( 'categories' );
            $cat_row   = $db->getRow(
                "SELECT lft, rgt FROM {$cat_table} WHERE id = ?i",
                $id
            );
            if ( $cat_row ) {
                $product_count = (int) $db->getField(
                    "SELECT COUNT(DISTINCT pc.product_id)
                     FROM {$pc} pc
                     INNER JOIN {$cat_table} c ON c.id = pc.category_id
                     INNER JOIN {$pr} p ON p.id = pc.product_id
                     WHERE c.lft >= ?i AND c.rgt <= ?i
                       AND c.deleted_at IS NULL
                       AND p.deleted_at IS NULL",
                    (int) $cat_row['lft'],
                    (int) $cat_row['rgt']
                );
            }
        }

        if ( $product_count > 0 && ! $force ) {
            return new WP_Error(
                'tc_category_has_products',
                sprintf(
                    /* translators: %d = product count */
                    __( 'This category contains %d product(s). Are you sure you want to delete it?', 'titancart-lite' ),
                    $product_count
                ),
                [ 'status' => 409, 'product_count' => $product_count ]
            );
        }

        // ── Perform the delete ───────────────────────────────────────────────
        $this->repo->delete( $id, $recurse );

        // Clean up junction table for deleted category (and children if recursive).
        if ( $recurse ) {
            $cat_table = $db->table( 'categories' );
            $db->query(
                "DELETE pc FROM {$pc} pc
                 INNER JOIN {$cat_table} c ON c.id = pc.category_id
                 WHERE c.deleted_at IS NOT NULL"
            );
        } else {
            $db->query( "DELETE FROM {$pc} WHERE category_id = ?i", $id );
        }

        LogService::info( 'categories', 'delete', 'Category deleted: #' . $id . ' — ' . ( $cat['name'] ?? '' ) . ( $recurse ? ' (recursive)' : '' ), [
            'object_type'     => 'category',
            'object_id'       => $id,
            'category_name'   => $cat['name'] ?? '',
            'recursive'       => $recurse,
            'products_unlinked' => $product_count,
        ] );

        return new WP_REST_Response( [ 'deleted' => true, 'id' => $id ] );
    }

    /**
     * POST /categories/reorder
     *
     * Body:
     *   parent_id    int   Parent of the siblings being reordered (0 for top-level).
     *   ordered_ids  int[] Full sibling set in the desired order.
     *
     * All ids must belong to the given parent_id. Partial reorders (a subset
     * of siblings) are rejected — the frontend must send the complete sibling
     * list.
     */
    public function reorder_items( $request ): WP_REST_Response|WP_Error {
        $parent_id_raw = $request->get_param( 'parent_id' );
        if ( $parent_id_raw === null ) {
            return new WP_Error(
                'tc_reorder_missing_parent',
                __( 'parent_id is required.', 'titancart-lite' ),
                [ 'status' => 400 ]
            );
        }
        $parent_id = (int) $parent_id_raw;

        $ordered_raw = $request->get_param( 'ordered_ids' );
        if ( ! is_array( $ordered_raw ) || empty( $ordered_raw ) ) {
            return new WP_Error(
                'tc_reorder_missing_ids',
                __( 'ordered_ids must be a non-empty array.', 'titancart-lite' ),
                [ 'status' => 400 ]
            );
        }
        $ordered_ids = array_values( array_filter( array_map( 'intval', $ordered_raw ), function( $n ) {
            return $n > 0;
        } ) );

        if ( empty( $ordered_ids ) ) {
            return new WP_Error(
                'tc_reorder_invalid_ids',
                __( 'ordered_ids contained no valid category IDs.', 'titancart-lite' ),
                [ 'status' => 400 ]
            );
        }

        // ── Atomic reorder ───────────────────────────────────────────────────
        // CategoryRepository::reorder() docstring explicitly says "Caller is
        // responsible for transaction wrapping." Sibling reorder + nested-set
        // rebuild is N+M UPDATE statements — partial application would leave
        // lft/rgt/depth inconsistent with parent_id+position.
        $db    = titancart_db();
        $count = 0;
        $db->begin();
        try {
            $count = $this->repo->reorder( $parent_id, $ordered_ids );
            $db->commit();
        } catch ( \Throwable $e ) {
            $db->rollback();
            LogService::info( 'categories', 'reorder', 'Categories reorder failed under parent #' . $parent_id . ': ' . $e->getMessage(), [
                'object_type' => 'category',
                'object_id'   => $parent_id,
                'parent_id'   => $parent_id,
                'id_count'    => count( $ordered_ids ),
                'ordered_ids' => $ordered_ids,
                'error'       => $e->getMessage(),
            ] );
            return new WP_Error(
                'tc_reorder_failed',
                __( 'Reorder failed due to a database error.', 'titancart-lite' ),
                [ 'status' => 500 ]
            );
        }

        LogService::info( 'categories', 'reorder', 'Categories reordered under parent #' . $parent_id, [
            'object_type' => 'category',
            'object_id'   => $parent_id,
            'parent_id'   => $parent_id,
            'id_count'    => count( $ordered_ids ),
            'reordered'   => $count,
            'ordered_ids' => $ordered_ids,
        ] );

        return new WP_REST_Response( [
            'reordered'   => $count,
            'parent_id'   => $parent_id,
            'ordered_ids' => $ordered_ids,
        ] );
    }

    // ── Permissions ───────────────────────────────────────────────────────────

    public function get_items_permissions_check( $request ): bool { return current_user_can( 'manage_options' ); }
    public function get_item_permissions_check( $request ): bool  { return current_user_can( 'manage_options' ); }
    public function create_item_permissions_check( $request ): bool { return current_user_can( 'manage_options' ); }
    public function update_item_permissions_check( $request ): bool { return current_user_can( 'manage_options' ); }
    public function delete_item_permissions_check( $request ): bool { return current_user_can( 'manage_options' ); }

    // ── Image URL enrichment ─────────────────────────────────────────────────

    /**
     * Resolve image_id → image_url for a single category row.
     *
     * @param  array|null $cat Category data array.
     * @return array|null      Same array with 'image_url' appended.
     */
    private function enrich_image( ?array $cat ): ?array {
        if ( ! $cat ) {
            return $cat;
        }

        $cat['image_url'] = '';
        if ( ! empty( $cat['image_id'] ) ) {
            $url = wp_get_attachment_image_url( (int) $cat['image_id'], 'medium' );
            if ( $url ) {
                $cat['image_url'] = $url;
            }
        }

        return $cat;
    }

    /**
     * Resolve image URLs for an array of category rows (flat list).
     *
     * @param  array $cats
     * @return array
     */
    private function enrich_images_list( array $cats ): array {
        foreach ( $cats as &$cat ) {
            $cat = $this->enrich_image( $cat );
        }
        unset( $cat );
        return $cats;
    }

    /**
     * Recursively resolve image URLs in a nested category tree.
     *
     * @param  array $tree
     * @return array
     */
    private function enrich_images_tree( array $tree ): array {
        foreach ( $tree as &$node ) {
            $node = $this->enrich_image( $node );
            if ( ! empty( $node['children'] ) && is_array( $node['children'] ) ) {
                $node['children'] = $this->enrich_images_tree( $node['children'] );
            }
        }
        unset( $node );
        return $tree;
    }

    // ── Input sanitization ────────────────────────────────────────────────────

    private function sanitize_category_input( WP_REST_Request $request ): array {
        $data = [];

        if ( $request->has_param( 'parent_id' ) ) {
            $data['parent_id'] = (int) $request->get_param( 'parent_id' );
        }
        if ( $request->has_param( 'position' ) ) {
            $data['position'] = (int) $request->get_param( 'position' );
        }
        if ( $request->has_param( 'status' ) ) {
            $data['status'] = sanitize_text_field( $request->get_param( 'status' ) );
        }
        if ( $request->has_param( 'slug' ) ) {
            $data['slug'] = sanitize_title( $request->get_param( 'slug' ) );
        }
        if ( $request->has_param( 'image_id' ) ) {
            $img_id = (int) $request->get_param( 'image_id' );
            $data['image_id'] = $img_id > 0 ? $img_id : null;
        }
        if ( $request->has_param( 'age_restricted' ) ) {
            $data['age_restricted'] = (int) ( ! empty( $request->get_param( 'age_restricted' ) ) );
        }
        if ( $request->has_param( 'age_limit' ) ) {
            $data['age_limit'] = max( 0, (int) $request->get_param( 'age_limit' ) );
        }

        $translation = $request->get_param( 'translation' );
        if ( is_array( $translation ) ) {
            $data['translation'] = [
                'name'        => sanitize_text_field( $translation['name'] ?? '' ),
                'description' => wp_kses_post( $translation['description'] ?? '' ),
                'meta_title'  => sanitize_text_field( $translation['meta_title'] ?? '' ),
                'meta_desc'   => sanitize_text_field( $translation['meta_desc'] ?? '' ),
            ];
        }

        return $data;
    }

    public function get_collection_params(): array {
        return [
            'page'      => [ 'type' => 'integer', 'default' => 1 ],
            'per_page'  => [ 'type' => 'integer', 'default' => 0 ],
            'search'    => [ 'type' => 'string',  'default' => '' ],
            'status'    => [ 'type' => 'string',  'default' => '' ],
            'parent_id' => [ 'type' => 'integer' ],
            'sort_by'   => [ 'type' => 'string',  'default' => 'position' ],
            'sort_order'=> [ 'type' => 'string',  'default' => 'asc' ],
            'lang'      => [ 'type' => 'string',  'default' => 'en' ],
        ];
    }
}
