<?php
/**
 * TitanCart Import/Export REST Controller — Module 13
 *
 * Registers endpoints under /wp-json/titancart/v1/:
 *
 *   POST   /import/upload              Upload CSV, get job_id + preview + field map
 *   POST   /import/validate            Dry-run validate with column mapping
 *   POST   /import/start               Commit column map, schedule import cron
 *   GET    /import/progress/{job_id}   Poll job status and progress %
 *   GET    /import/jobs                List recent import jobs
 *   GET    /import/fields/{type}       Get field map for a given entity type
 *
 *   Import presets (KI-34 — Pro "Advanced Products Import", gated on
 *   is_active('advanced-import')):
 *   GET    /import/presets             List presets (optional ?type=)
 *   POST   /import/presets             Create a preset { name, type, column_map, options }
 *   PUT    /import/presets/{id}        Update a preset { name?, column_map?, options? }
 *   DELETE /import/presets/{id}        Delete a preset
 *
 *   GET    /export/columns/{type}      Available columns for export type
 *   POST   /export/generate            Generate CSV and return download URL
 *   GET    /export/download/{job_id}   (future — currently generate returns URL directly)
 *
 *   Prism demo system (DEMO_IMPORT_PLAN.md, S471):
 *   GET    /export/demo/presets        Presets exportable from this (source) store
 *   POST   /export/demo                Build hosted package(s) { presets: [] }
 *   GET    /demo/status                Installed preset, lock state, counts, job
 *   POST   /demo/install               { url, force? } → creates the stepped job
 *   POST   /demo/step                  Run the next slice; poll until done/error
 *   POST   /demo/remove                Remove the installed demo (both layers)
 *   POST   /demo/clear-sample-data     Remove generated customers/orders/reviews only
 *
 * All routes require manage_options capability.
 *
 * @package TitanCart\REST\ImportExport
 */

namespace TitanCart\REST\ImportExport;

use TitanCart\Modules\ImportExport\ImportService;
use TitanCart\Modules\ImportExport\ExportService;
use TitanCart\Modules\ImportExport\DemoExportService;
use TitanCart\Modules\ImportExport\DemoImportService;
use TitanCart\Core\LogService;

defined( 'ABSPATH' ) || exit;

class ImportExportController {

	const NAMESPACE = 'titancart/v1';

	public function register_routes(): void {

		// ── Import ────────────────────────────────────────────────────────────

		register_rest_route( self::NAMESPACE, '/import/upload', [
			'methods'             => 'POST',
			'callback'            => [ $this, 'upload' ],
			'permission_callback' => [ $this, 'adminOnly' ],
		] );

		register_rest_route( self::NAMESPACE, '/import/validate', [
			'methods'             => 'POST',
			'callback'            => [ $this, 'validate' ],
			'permission_callback' => [ $this, 'adminOnly' ],
			'args'                => [
				'job_id'     => [ 'required' => true,  'type' => 'integer' ],
				'column_map' => [ 'required' => true,  'type' => 'object'  ],
			],
		] );

		register_rest_route( self::NAMESPACE, '/import/start', [
			'methods'             => 'POST',
			'callback'            => [ $this, 'start' ],
			'permission_callback' => [ $this, 'adminOnly' ],
			'args'                => [
				'job_id'     => [ 'required' => true, 'type' => 'integer' ],
				'column_map' => [ 'required' => true, 'type' => 'object'  ],
			],
		] );

		register_rest_route( self::NAMESPACE, '/import/progress/(?P<job_id>\d+)', [
			'methods'             => 'GET',
			'callback'            => [ $this, 'progress' ],
			'permission_callback' => [ $this, 'adminOnly' ],
			'args'                => [
				'job_id' => [ 'required' => true, 'type' => 'integer' ],
			],
		] );

		register_rest_route( self::NAMESPACE, '/import/jobs', [
			'methods'             => 'GET',
			'callback'            => [ $this, 'listJobs' ],
			'permission_callback' => [ $this, 'adminOnly' ],
		] );

		register_rest_route( self::NAMESPACE, '/import/fields/(?P<type>[a-z_]+)', [
			'methods'             => 'GET',
			'callback'            => [ $this, 'getFields' ],
			'permission_callback' => [ $this, 'adminOnly' ],
			'args'                => [
				'type' => [ 'required' => true, 'type' => 'string' ],
			],
		] );


		// ── Export ────────────────────────────────────────────────────────────

		register_rest_route( self::NAMESPACE, '/export/columns/(?P<type>[a-z_]+)', [
			'methods'             => 'GET',
			'callback'            => [ $this, 'getColumns' ],
			'permission_callback' => [ $this, 'adminOnly' ],
			'args'                => [
				'type' => [ 'required' => true, 'type' => 'string' ],
			],
		] );

		register_rest_route( self::NAMESPACE, '/export/generate', [
			'methods'             => 'POST',
			'callback'            => [ $this, 'generate' ],
			'permission_callback' => [ $this, 'adminOnly' ],
			'args'                => [
				'type'    => [ 'required' => true, 'type' => 'string' ],
				'columns' => [ 'required' => false, 'type' => 'array', 'default' => [] ],
				'filters' => [ 'required' => false, 'type' => 'object', 'default' => [] ],
			],
		] );

		// ── Demo packages (Prism demo importer — DEMO_IMPORT_PLAN.md) ───────────
		// Source-store side: builds the hosted per-preset package(s) that
		// market.titancart.io/demos/ serves. Run on the demo source store only.

		register_rest_route( self::NAMESPACE, '/export/demo/presets', [
			'methods'             => 'GET',
			'callback'            => [ $this, 'demoPresets' ],
			'permission_callback' => [ $this, 'adminOnly' ],
		] );

		register_rest_route( self::NAMESPACE, '/export/demo', [
			'methods'             => 'POST',
			'callback'            => [ $this, 'demoExport' ],
			'permission_callback' => [ $this, 'adminOnly' ],
			'args'                => [
				// [] or omitted = every preset discovered in theme_options.
				'presets' => [ 'required' => false, 'type' => 'array', 'default' => [] ],
			],
		] );

		// Store side: install / switch / remove a demo package (Prism 0.2.0 buttons).
		register_rest_route( self::NAMESPACE, '/demo/status', [
			'methods'             => 'GET',
			'callback'            => [ $this, 'demoStatus' ],
			'permission_callback' => [ $this, 'adminOnly' ],
		] );

		register_rest_route( self::NAMESPACE, '/demo/manifest', [
			'methods'             => 'GET',
			'callback'            => [ $this, 'demoManifest' ],
			'permission_callback' => [ $this, 'adminOnly' ],
			'args'                => [
				'url' => [ 'required' => true, 'type' => 'string' ],
			],
		] );

		register_rest_route( self::NAMESPACE, '/demo/install', [
			'methods'             => 'POST',
			'callback'            => [ $this, 'demoInstall' ],
			'permission_callback' => [ $this, 'adminOnly' ],
			'args'                => [
				'url'   => [ 'required' => true, 'type' => 'string' ],
				'force' => [ 'required' => false, 'type' => 'boolean', 'default' => false ],
			],
		] );

		register_rest_route( self::NAMESPACE, '/demo/step', [
			'methods'             => 'POST',
			'callback'            => [ $this, 'demoStep' ],
			'permission_callback' => [ $this, 'adminOnly' ],
		] );

		register_rest_route( self::NAMESPACE, '/demo/remove', [
			'methods'             => 'POST',
			'callback'            => [ $this, 'demoRemove' ],
			'permission_callback' => [ $this, 'adminOnly' ],
		] );

		register_rest_route( self::NAMESPACE, '/demo/clear-sample-data', [
			'methods'             => 'POST',
			'callback'            => [ $this, 'demoClearSample' ],
			'permission_callback' => [ $this, 'adminOnly' ],
		] );

		// ── Flat aliases (tester / external compat) ────────────────────────────

		register_rest_route( self::NAMESPACE, '/import/logs', [
			'methods'             => 'GET',
			'callback'            => [ $this, 'listJobs' ],
			'permission_callback' => [ $this, 'adminOnly' ],
		] );

		register_rest_route( self::NAMESPACE, '/export/jobs', [
			'methods'             => 'GET',
			'callback'            => [ $this, 'listJobs' ],
			'permission_callback' => [ $this, 'adminOnly' ],
		] );
	}

	// ──────────────────────────────────────────────────────────────────────────
	// Import handlers
	// ──────────────────────────────────────────────────────────────────────────

	/**
	 * POST /import/upload
	 * Expects a multipart form upload: file (CSV), type (string), options (JSON object).
	 */
	public function upload( \WP_REST_Request $request ): \WP_REST_Response {
		$files = $request->get_file_params();

		if ( empty( $files['file'] ) ) {
			return new \WP_REST_Response( [ 'error' => 'No file uploaded. Send the CSV as multipart field "file".' ], 400 );
		}

		$type = sanitize_key( $request->get_param( 'type' ) ?: 'products' );
		$raw_options = $request->get_param( 'options' );
		$options = is_array( $raw_options ) ? $raw_options : [];

		try {
			$result = ( new ImportService() )->upload( $files['file'], $type, $options );
			return new \WP_REST_Response( $result, 200 );
		} catch ( \Throwable $e ) {
			return new \WP_REST_Response( [ 'error' => $e->getMessage() ], 422 );
		}
	}

	/**
	 * POST /import/validate
	 */
	public function validate( \WP_REST_Request $request ): \WP_REST_Response {
		$job_id     = (int) $request->get_param( 'job_id' );
		$column_map = (array) $request->get_param( 'column_map' );

		// Normalise: the UI may send [ { index: 0, field: 'name' }, ... ]
		// or a simple object { "0": "name", "1": "sku" }.
		// We normalise to the latter (int-keyed array).
		$column_map = $this->normaliseColumnMap( $column_map );

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

	/**
	 * POST /import/start
	 */
	public function start( \WP_REST_Request $request ): \WP_REST_Response {
		$job_id     = (int) $request->get_param( 'job_id' );
		$column_map = $this->normaliseColumnMap( (array) $request->get_param( 'column_map' ) );

		try {
			$ok = ( new ImportService() )->startJob( $job_id, $column_map );
			if ( ! $ok ) {
				return new \WP_REST_Response( [ 'error' => "Job #{$job_id} could not be started. It may not exist or is already running." ], 409 );
			}

			LogService::info( 'general', 'create', 'Import job started: #' . $job_id, [
				'object_type' => 'import_job',
				'object_id'   => $job_id,
			] );

			return new \WP_REST_Response( [
				'job_id'  => $job_id,
				'message' => 'Import job queued. Poll /import/progress/' . $job_id . ' for updates.',
			], 200 );
		} catch ( \Throwable $e ) {
			return new \WP_REST_Response( [ 'error' => $e->getMessage() ], 422 );
		}
	}

	/**
	 * GET /import/progress/{job_id}
	 */
	public function progress( \WP_REST_Request $request ): \WP_REST_Response {
		$job_id = (int) $request->get_param( 'job_id' );

		$job = ( new ImportService() )->getProgress( $job_id );
		if ( ! $job ) {
			return new \WP_REST_Response( [ 'error' => "Job #{$job_id} not found." ], 404 );
		}

		$total     = max( 1, (int) $job->total_rows );
		$processed = (int) $job->processed;

		return new \WP_REST_Response( [
			'id'           => (int) $job->id,
			'type'         => $job->type,
			'filename'     => $job->filename,
			'status'       => $job->status,
			'total_rows'   => $total,
			'processed'    => $processed,
			'errors'       => (int) $job->errors,
			'percent'      => (int) round( ( $processed / $total ) * 100 ),
			'log'          => $job->log,
			'started_at'   => $job->started_at,
			'completed_at' => $job->completed_at,
			'created_at'   => $job->created_at,
		], 200 );
	}

	/**
	 * GET /import/jobs
	 */
	public function listJobs( \WP_REST_Request $request ): \WP_REST_Response {
		$limit  = (int) ( $request->get_param( 'per_page' ) ?: 20 );
		$offset = ( (int) ( $request->get_param( 'page' ) ?: 1 ) - 1 ) * $limit;

		$jobs = ( new ImportService() )->getJobs( $limit, $offset );

		return new \WP_REST_Response( array_map( function ( $job ) {
			$total     = max( 1, (int) $job->total_rows );
			$processed = (int) $job->processed;
			return [
				'id'           => (int) $job->id,
				'type'         => $job->type,
				'filename'     => $job->filename,
				'status'       => $job->status,
				'total_rows'   => $total,
				'processed'    => $processed,
				'errors'       => (int) $job->errors,
				'percent'      => (int) round( ( $processed / $total ) * 100 ),
				'created_at'   => $job->created_at,
				'completed_at' => $job->completed_at,
			];
		}, $jobs ), 200 );
	}

	/**
	 * GET /import/fields/{type}
	 */
	public function getFields( \WP_REST_Request $request ): \WP_REST_Response {
		$type = sanitize_key( $request->get_param( 'type' ) );
		$map  = ( new ImportService() )->getFieldMap( $type );
		return new \WP_REST_Response( $map, 200 );
	}


	// ──────────────────────────────────────────────────────────────────────────
	// Export handlers
	// ──────────────────────────────────────────────────────────────────────────

	/**
	 * GET /export/columns/{type}
	 */
	public function getColumns( \WP_REST_Request $request ): \WP_REST_Response {
		$type    = sanitize_key( $request->get_param( 'type' ) );
		$columns = ( new ExportService() )->getColumns( $type );

		// Return as array of { key, label } for easy UI rendering.
		$result = [];
		foreach ( $columns as $key => $label ) {
			$result[] = [ 'key' => $key, 'label' => $label ];
		}

		return new \WP_REST_Response( $result, 200 );
	}

	/**
	 * POST /export/generate
	 */
	public function generate( \WP_REST_Request $request ): \WP_REST_Response {
		$type    = sanitize_key( $request->get_param( 'type' ) ?: 'products' );
		$columns = array_map( 'sanitize_key', (array) ( $request->get_param( 'columns' ) ?: [] ) );
		$filters = (array) ( $request->get_param( 'filters' ) ?: [] );

		// Sanitise filter values.
		$safe_filters = [];
		foreach ( [ 'status', 'date_from', 'date_to' ] as $k ) {
			if ( ! empty( $filters[ $k ] ) ) {
				$safe_filters[ $k ] = sanitize_text_field( $filters[ $k ] );
			}
		}

		try {
			$result = ( new ExportService() )->generate( $type, $safe_filters, $columns );

			LogService::info( 'general', 'create', 'Export generated: ' . $type . ' (' . ( $result['rows'] ?? 0 ) . ' rows)', [
				'object_type' => 'export',
				'type'        => $type,
				'rows'        => $result['rows'] ?? 0,
			] );

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

	/**
	 * GET /export/demo/presets — preset ids exportable from this store.
	 */
	public function demoPresets( \WP_REST_Request $request ): \WP_REST_Response {
		return new \WP_REST_Response( [ 'presets' => ( new DemoExportService() )->discoverPresets() ], 200 );
	}

	/**
	 * POST /export/demo — build package(s). Body: { presets: ['apparel', ...] }
	 * or an empty list for every discoverable preset. Returns per-preset
	 * results (zip path/url + counts) or an error string per failed preset.
	 */
	public function demoExport( \WP_REST_Request $request ): \WP_REST_Response {
		$svc     = new DemoExportService();
		$presets = array_values( array_filter( array_map( 'sanitize_key', (array) ( $request->get_param( 'presets' ) ?: [] ) ) ) );
		if ( empty( $presets ) ) {
			$presets = $svc->discoverPresets();
		}
		if ( empty( $presets ) ) {
			return new \WP_REST_Response( [ 'error' => 'No presets found in theme_options on this store.' ], 422 );
		}

		// Package builds copy every image; give the request room.
		if ( function_exists( 'set_time_limit' ) ) {
			@set_time_limit( 600 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- best-effort on hosts that disallow it.
		}

		$results = $svc->exportPresets( $presets );

		$ok = array_filter( $results, fn( $r ) => empty( $r['error'] ) );
		LogService::info( 'general', 'create', 'Demo packages exported: ' . implode( ', ', array_keys( $ok ) ), [
			'object_type' => 'demo_export',
			'presets'     => $presets,
		] );

		return new \WP_REST_Response( [ 'results' => $results ], 200 );
	}

	/** GET /demo/status — installed preset, lock state + reasons, log counts, active job. */
	public function demoStatus( \WP_REST_Request $request ): \WP_REST_Response {
		return new \WP_REST_Response( ( new DemoImportService() )->status(), 200 );
	}

	/**
	 * GET /demo/manifest?url= — server-side fetch of the hosted demos index
	 * (no CORS needed on market). Shape returned to the SPA:
	 *   { presets: { <id>: { package_url, label?, version?, products? } } }
	 * Relative `package` paths in the index resolve against the index URL.
	 * Cached 10 minutes per URL.
	 */
	public function demoManifest( \WP_REST_Request $request ): \WP_REST_Response {
		$url = esc_url_raw( (string) $request->get_param( 'url' ) );
		if ( ! preg_match( '#^https://#i', $url ) ) {
			return new \WP_REST_Response( [ 'error' => 'Manifest URL must be https.' ], 422 );
		}
		$cache_key = 'titancart_demo_manifest_' . md5( $url );
		$cached    = get_transient( $cache_key );
		if ( is_array( $cached ) ) {
			return new \WP_REST_Response( $cached, 200 );
		}
		$resp = wp_remote_get( $url, [ 'timeout' => 20 ] );
		if ( is_wp_error( $resp ) || 200 !== (int) wp_remote_retrieve_response_code( $resp ) ) {
			return new \WP_REST_Response( [ 'presets' => [], 'error' => 'Demo index unavailable.' ], 200 );
		}
		$idx  = json_decode( wp_remote_retrieve_body( $resp ), true );
		$base = rtrim( dirname( $url ), '/' ) . '/';
		$out  = [ 'presets' => [] ];
		foreach ( (array) ( $idx['presets'] ?? [] ) as $id => $entry ) {
			$id = sanitize_key( (string) $id );
			if ( '' === $id || ! is_array( $entry ) || empty( $entry['package'] ) ) {
				continue;
			}
			$pkg = (string) $entry['package'];
			$out['presets'][ $id ] = [
				'package_url' => preg_match( '#^https://#i', $pkg ) ? $pkg : $base . ltrim( $pkg, '/' ),
				'label'       => sanitize_text_field( (string) ( $entry['label'] ?? '' ) ),
				'version'     => sanitize_text_field( (string) ( $entry['version'] ?? '' ) ),
				'products'    => (int) ( $entry['products'] ?? 0 ),
			];
		}
		set_transient( $cache_key, $out, 10 * MINUTE_IN_SECONDS );
		return new \WP_REST_Response( $out, 200 );
	}

	/**
	 * POST /demo/install { url, force? } — fetch the package, dirty-check
	 * (unless force), and create the client-stepped job. The UI then calls
	 * /demo/step until status is 'done' or 'error'.
	 */
	public function demoInstall( \WP_REST_Request $request ): \WP_REST_Response {
		try {
			$job = ( new DemoImportService() )->startInstall(
				esc_url_raw( (string) $request->get_param( 'url' ) ),
				(bool) $request->get_param( 'force' )
			);
			LogService::info( 'general', 'create', 'Demo install started: ' . $job['preset'], [ 'object_type' => 'demo_import', 'preset' => $job['preset'] ] );
			return new \WP_REST_Response( $job, 200 );
		} catch ( \Throwable $e ) {
			return new \WP_REST_Response( [ 'error' => $e->getMessage() ], 422 );
		}
	}

	/** POST /demo/step — run the next slice of the active job. */
	public function demoStep( \WP_REST_Request $request ): \WP_REST_Response {
		if ( function_exists( 'set_time_limit' ) ) {
			@set_time_limit( 120 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- best-effort; a chunk sideloads several images.
		}
		return new \WP_REST_Response( ( new DemoImportService() )->step(), 200 );
	}

	/** POST /demo/remove — remove the installed demo entirely (both layers). */
	public function demoRemove( \WP_REST_Request $request ): \WP_REST_Response {
		$svc    = new DemoImportService();
		$status = $svc->status();
		if ( empty( $status['installed']['preset'] ) ) {
			return new \WP_REST_Response( [ 'error' => 'No demo is installed.' ], 422 );
		}
		try {
			$removed = $svc->remove( (string) $status['installed']['preset'] );
			LogService::info( 'general', 'delete', 'Demo removed: ' . $status['installed']['preset'], [ 'object_type' => 'demo_import', 'removed' => $removed ] );
			return new \WP_REST_Response( [ 'removed' => $removed, 'status' => $svc->status() ], 200 );
		} catch ( \Throwable $e ) {
			return new \WP_REST_Response( [ 'error' => $e->getMessage() ], 422 );
		}
	}

	/** POST /demo/clear-sample-data — drop generated customers/orders/reviews only. */
	public function demoClearSample( \WP_REST_Request $request ): \WP_REST_Response {
		$svc = new DemoImportService();
		try {
			$removed = $svc->clearSampleData();
			LogService::info( 'general', 'delete', 'Demo sample data cleared', [ 'object_type' => 'demo_import', 'removed' => $removed ] );
			return new \WP_REST_Response( [ 'removed' => $removed, 'status' => $svc->status() ], 200 );
		} catch ( \Throwable $e ) {
			return new \WP_REST_Response( [ 'error' => $e->getMessage() ], 422 );
		}
	}

	// ──────────────────────────────────────────────────────────────────────────
	// Helpers
	// ──────────────────────────────────────────────────────────────────────────

	public function adminOnly(): bool {
		return current_user_can( 'manage_options' );
	}


	/**
	 * Normalise column map to int-indexed [ col_index => tc_field ] format.
	 *
	 * Accepts either:
	 *   { "0": "name", "1": "sku", "2": "" }
	 * or:
	 *   [ { "index": 0, "field": "name" }, ... ]
	 *
	 * @param  array $raw
	 * @return array<int, string>
	 */
	private function normaliseColumnMap( array $raw ): array {
		$result = [];

		// Detect object-of-objects format.
		$first = reset( $raw );
		if ( is_array( $first ) && isset( $first['index'] ) ) {
			foreach ( $raw as $item ) {
				$result[ (int) $item['index'] ] = $this->sanitiseFieldKey( $item['field'] ?? '' );
			}
		} else {
			foreach ( $raw as $k => $v ) {
				$result[ (int) $k ] = $this->sanitiseFieldKey( (string) $v );
			}
		}

		return $result;
	}

	/**
	 * Sanitise a field key, preserving the "feature:" prefix that sanitize_key() would strip.
	 */
	private function sanitiseFieldKey( string $key ): string {
		// Feature keys use a colon separator: "feature:123"
		if ( preg_match( '/^feature:(\d+)$/', $key, $m ) ) {
			return 'feature:' . $m[1];
		}
		return sanitize_key( $key );
	}
}
