<?php
/**
 * AJAX endpoints for the interactive admin UI.
 *
 * All endpoints:
 *   - Validate the shared 'lens_admin' nonce
 *   - Enforce capabilities (manage_options for most; edit_posts for scans)
 *   - Return JSON via wp_send_json_success() / wp_send_json_error()
 *
 * @package WPLens
 */

namespace WPLens\Admin;

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

// phpcs:disable WordPress.Security.NonceVerification.Missing
// phpcs:disable WordPress.Security.NonceVerification.Recommended
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
// phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
// phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
// phpcs:disable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
// phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter
//
// Justification:
//   1. Every endpoint is dispatched through dispatch() which calls
//      check_ajax_referer( self::NONCE_ACTION, '_wpnonce', false ) and
//      current_user_can( $capability ) before any handler runs. PHPCS
//      cannot trace the dispatch() -> handle_X() chain so every $_POST
//      access inside handlers is flagged as missing nonce verification.
//   2. SQL queries that aggregate the analytics tables interpolate the
//      table name (built from $wpdb->prefix . a static suffix, which is
//      safe by construction) and use array placeholders built via
//      array_fill() — PHPCS cannot prove the placeholder/argument count
//      matches at static-analysis time.

final class Ajax {

	public const NONCE_ACTION = 'lens_admin';

	public function register(): void {
		$endpoints = array(
			'search_posts'      => 'edit_posts',
			'run_scan'          => 'edit_posts',
			'get_scan'          => 'edit_posts',
			'bulk_scan_targets' => 'edit_posts',
			'add_query'         => 'manage_options',
			'delete_query'      => 'manage_options',
			'run_citation'      => 'manage_options',
			'run_all_citations' => 'manage_options',
			'test_provider'     => 'manage_options',
			'dashboard_data'    => 'manage_options',
			'citations_data'    => 'manage_options',
		);

		foreach ( $endpoints as $action => $capability ) {
			add_action( 'wp_ajax_lens_' . $action, function () use ( $action, $capability ) {
				$this->dispatch( $action, $capability );
			} );
		}
	}

	private function dispatch( string $action, string $capability ): void {
		if ( ! check_ajax_referer( self::NONCE_ACTION, '_wpnonce', false ) ) {
			wp_send_json_error( array( 'message' => __( 'Security check failed. Please refresh the page and try again.', 'lens-ai-visibility-monitor' ) ), 403 );
		}
		if ( ! current_user_can( $capability ) ) {
			wp_send_json_error( array( 'message' => __( 'You do not have permission.', 'lens-ai-visibility-monitor' ) ), 403 );
		}

		$method = 'handle_' . $action;
		if ( ! method_exists( $this, $method ) ) {
			wp_send_json_error( array( 'message' => __( 'Unknown action.', 'lens-ai-visibility-monitor' ) ), 400 );
		}
		$this->$method();
	}

	// ---------------------------------------------------------------- post search

	private function handle_search_posts(): void {
		$term = isset( $_POST['term'] ) ? sanitize_text_field( wp_unslash( $_POST['term'] ) ) : '';
		if ( strlen( $term ) < 2 ) {
			wp_send_json_success( array( 'items' => array() ) );
		}

		$q = new \WP_Query( array(
			'post_type'      => array( 'post', 'page' ),
			'post_status'    => array( 'publish', 'draft' ),
			'posts_per_page' => 10,
			's'              => $term,
			'orderby'        => 'relevance',
			'no_found_rows'  => true,
		) );

		$items = array();
		foreach ( $q->posts as $p ) {
			$items[] = array(
				'id'        => (int) $p->ID,
				'title'     => '' !== $p->post_title ? $p->post_title : __( '(no title)', 'lens-ai-visibility-monitor' ),
				'post_type' => $p->post_type,
				'status'    => $p->post_status,
			);
		}
		wp_send_json_success( array( 'items' => $items ) );
	}

	// ---------------------------------------------------------------- scans

	private function handle_run_scan(): void {
		$post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
		$with_ai = ! empty( $_POST['with_ai'] );
		if ( $post_id < 1 ) {
			wp_send_json_error( array( 'message' => __( 'Please select a post.', 'lens-ai-visibility-monitor' ) ), 400 );
		}
		$runner = new \WPLens\Scans\ScanRunner();
		$result = $runner->scan_post( $post_id, $with_ai );
		if ( empty( $result['ok'] ) ) {
			wp_send_json_error( array( 'message' => $result['error'] ?? __( 'Scan failed.', 'lens-ai-visibility-monitor' ) ) );
		}
		// Reshape findings for client.
		$findings = $result['findings']['local'] ?? array();
		$cleaned  = array();
		foreach ( $findings as $key => $f ) {
			$cleaned[] = array(
				'key'     => (string) $key,
				'pass'    => ! empty( $f['pass'] ),
				'severity'=> $f['severity'] ?? 'normal',
				'message' => (string) ( $f['message'] ?? '' ),
			);
		}
		wp_send_json_success( array(
			'scan_id'  => (int) ( $result['scan_id'] ?? 0 ),
			'score'    => $result['score'] ?? null,
			'findings' => $cleaned,
			'ai_text'  => (string) ( $result['findings']['ai_text'] ?? '' ),
			'ai_score' => $result['findings']['ai_score'] ?? null,
			'post_id'  => $post_id,
			'post_title' => get_the_title( $post_id ),
		) );
	}

	private function handle_get_scan(): void {
		$scan_id = isset( $_POST['scan_id'] ) ? absint( $_POST['scan_id'] ) : 0;
		if ( $scan_id < 1 ) {
			wp_send_json_error( array( 'message' => __( 'Invalid scan ID.', 'lens-ai-visibility-monitor' ) ), 400 );
		}
		$repo = new \WPLens\Scans\ScanRepository();
		$row  = $repo->find( $scan_id );
		if ( ! $row ) {
			wp_send_json_error( array( 'message' => __( 'Scan not found.', 'lens-ai-visibility-monitor' ) ), 404 );
		}
		$row['findings_data'] = json_decode( (string) ( $row['findings'] ?? '{}' ), true );
		unset( $row['findings'] );
		wp_send_json_success( $row );
	}

	// ---------------------------------------------------------------- citations

	private function handle_add_query(): void {
		$query = isset( $_POST['query'] ) ? sanitize_text_field( wp_unslash( $_POST['query'] ) ) : '';
		if ( '' === $query ) {
			wp_send_json_error( array( 'message' => __( 'Query cannot be empty.', 'lens-ai-visibility-monitor' ) ), 400 );
		}
		$repo = new \WPLens\Citations\CitationRepository();
		$id   = $repo->add_query( $query );
		if ( $id < 1 ) {
			wp_send_json_error( array( 'message' => __( 'Could not save query.', 'lens-ai-visibility-monitor' ) ) );
		}
		wp_send_json_success( array(
			'id'         => $id,
			'query'      => $query,
			'is_active'  => 1,
			'created_at' => current_time( 'mysql', true ),
		) );
	}

	private function handle_delete_query(): void {
		$id = isset( $_POST['id'] ) ? absint( $_POST['id'] ) : 0;
		if ( $id < 1 ) {
			wp_send_json_error( array( 'message' => __( 'Invalid query ID.', 'lens-ai-visibility-monitor' ) ), 400 );
		}
		$repo = new \WPLens\Citations\CitationRepository();
		$ok   = $repo->delete_query( $id );
		if ( ! $ok ) {
			wp_send_json_error( array( 'message' => __( 'Could not delete.', 'lens-ai-visibility-monitor' ) ) );
		}
		wp_send_json_success( array( 'id' => $id ) );
	}

	private function handle_run_citation(): void {
		$id = isset( $_POST['id'] ) ? absint( $_POST['id'] ) : 0;
		if ( $id < 1 ) {
			wp_send_json_error( array( 'message' => __( 'Invalid query ID.', 'lens-ai-visibility-monitor' ) ), 400 );
		}
		$runner = new \WPLens\Citations\CitationRunner();
		$result = $runner->run_single( $id );
		if ( empty( $result['ok'] ) ) {
			wp_send_json_error( array( 'message' => $result['error'] ?? __( 'Run failed.', 'lens-ai-visibility-monitor' ) ) );
		}
		wp_send_json_success( $result );
	}

	private function handle_run_all_citations(): void {
		$runner  = new \WPLens\Citations\CitationRunner();
		$results = $runner->run_active_queries();
		wp_send_json_success( array(
			'ran'    => count( $results ),
			'cited'  => count( array_filter( $results, static function ( $r ) { return ! empty( $r['cited'] ); } ) ),
		) );
	}

	// ---------------------------------------------------------------- providers

	private function handle_test_provider(): void {
		$provider_slug = isset( $_POST['provider'] )
			? sanitize_key( wp_unslash( $_POST['provider'] ) )
			: 'openai';
		$key_input = isset( $_POST['key'] )
			? sanitize_text_field( wp_unslash( $_POST['key'] ) )
			: '';

		// If a key was provided in the test request, use it transiently; otherwise use the stored key.
		if ( '' !== $key_input ) {
			$prov = new \WPLens\AI\OpenAIProvider();
			// Inject the test key by storing it temporarily — encrypted, then reverted.
			$existing = get_option( 'wplens_provider_keys', array() );
			$backup   = is_array( $existing ) ? $existing : array();
			$existing[ $provider_slug ] = \WPLens\Utils\Encryption::encrypt( $key_input );
			update_option( 'wplens_provider_keys', $existing, false );
		} else {
			$prov = \WPLens\AI\ProviderRegistry::get( $provider_slug );
			$backup = null;
		}

		if ( null === $prov || ! $prov->is_configured() ) {
			if ( null !== $backup ) {
				update_option( 'wplens_provider_keys', $backup, false );
			}
			wp_send_json_error( array( 'message' => __( 'No API key to test.', 'lens-ai-visibility-monitor' ) ) );
		}

		$result = $prov->generate(
			'You are a connectivity test. Reply with exactly the word: OK',
			'Are you reachable?',
			array( 'max_tokens' => 5, 'temperature' => 0 )
		);

		// Restore prior key state if this was a test of a candidate key.
		if ( null !== $backup ) {
			update_option( 'wplens_provider_keys', $backup, false );
		}

		if ( ! $result->ok ) {
			wp_send_json_error( array(
				'message' => $result->error_message ?? __( 'Could not reach the provider.', 'lens-ai-visibility-monitor' ),
				'code'    => $result->error_code,
			) );
		}
		wp_send_json_success( array(
			'message'        => __( 'Connection successful.', 'lens-ai-visibility-monitor' ),
			'input_tokens'   => $result->input_tokens,
			'output_tokens'  => $result->output_tokens,
		) );
	}

	// ---------------------------------------------------------------- dashboard data

	private function handle_dashboard_data(): void {
		$days = isset( $_POST['days'] ) ? absint( $_POST['days'] ) : 30;
		$days = max( 7, min( 365, $days ) );

		$repo       = new \WPLens\Crawler\LogRepository();
		$cite_repo  = new \WPLens\Citations\CitationRepository();
		$scan_repo  = new \WPLens\Scans\ScanRepository();

		// Build per-day series broken down by family using a single query for performance.
		global $wpdb;
		$table = $wpdb->prefix . 'wplens_crawler_log';
		$since = gmdate( 'Y-m-d 00:00:00', time() - $days * DAY_IN_SECONDS );
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.NoCaching
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT DATE(visited_at) AS day, bot_family, COUNT(*) AS visits FROM {$table} WHERE visited_at >= %s GROUP BY DATE(visited_at), bot_family ORDER BY day ASC",
				$since
			),
			ARRAY_A
		);

		$days_list = array();
		$end_ts    = time();
		for ( $i = $days - 1; $i >= 0; $i-- ) {
			$days_list[] = gmdate( 'Y-m-d', $end_ts - $i * DAY_IN_SECONDS );
		}

		$families = array();
		$by_day   = array();
		foreach ( $rows as $r ) {
			$d = (string) $r['day'];
			$f = (string) $r['bot_family'];
			$v = (int) $r['visits'];
			$families[ $f ] = ( $families[ $f ] ?? 0 ) + $v;
			$by_day[ $d ][ $f ] = $v;
		}

		// Build family series — one array per family with one value per day.
		$family_names = array_keys( $families );
		$series = array();
		foreach ( $family_names as $f ) {
			$values = array();
			foreach ( $days_list as $d ) {
				$values[] = (int) ( $by_day[ $d ][ $f ] ?? 0 );
			}
			$series[] = array( 'label' => $f, 'data' => $values, 'total' => $families[ $f ] );
		}

		wp_send_json_success( array(
			'days'         => $days_list,
			'series'       => $series,
			'totals_by_family' => $families,
			'total_visits' => array_sum( $families ),
			'top_pages'    => $repo->top_pages( $days, 10 ),
			'blind_spots'  => $repo->blind_spots( $days, 10 ),
			'citation_rate'=> $cite_repo->citation_rate( $days ),
			'recent_scans' => $scan_repo->recent( 5 ),
		) );
	}

	private function handle_citations_data(): void {
		$days = isset( $_POST['days'] ) ? absint( $_POST['days'] ) : 90;
		$days = max( 7, min( 365, $days ) );

		global $wpdb;
		$table = $wpdb->prefix . 'wplens_citation_checks';
		$since = gmdate( 'Y-m-d 00:00:00', time() - $days * DAY_IN_SECONDS );
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.NoCaching
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT DATE(checked_at) AS day, ai_provider, AVG(cited)*100 AS pct, COUNT(*) AS cnt FROM {$table} WHERE checked_at >= %s GROUP BY DATE(checked_at), ai_provider ORDER BY day ASC",
				$since
			),
			ARRAY_A
		);

		$days_list = array();
		$end_ts    = time();
		for ( $i = $days - 1; $i >= 0; $i-- ) {
			$days_list[] = gmdate( 'Y-m-d', $end_ts - $i * DAY_IN_SECONDS );
		}

		$by_provider = array();
		foreach ( $rows as $r ) {
			$by_provider[ (string) $r['ai_provider'] ][ (string) $r['day'] ] = (float) $r['pct'];
		}

		$series = array();
		foreach ( $by_provider as $prov => $data ) {
			$values = array();
			foreach ( $days_list as $d ) {
				$values[] = isset( $data[ $d ] ) ? round( $data[ $d ], 1 ) : null;
			}
			$series[] = array( 'label' => $prov, 'data' => $values );
		}

		wp_send_json_success( array( 'days' => $days_list, 'series' => $series ) );
	}

	// ---------------------------------------------------------------- bulk scan

	private function handle_bulk_scan_targets(): void {
		$post_type = isset( $_POST['post_type'] ) ? sanitize_key( wp_unslash( $_POST['post_type'] ) ) : 'any';
		$filter    = isset( $_POST['filter'] )    ? sanitize_key( wp_unslash( $_POST['filter'] ) )    : 'all';

		// Validate.
		$types = ( 'any' === $post_type ) ? array( 'post', 'page' ) : array( $post_type );
		$types = array_intersect( $types, array( 'post', 'page' ) );
		if ( empty( $types ) ) { $types = array( 'post', 'page' ); }

		$valid_filters = array( 'all', 'unscanned', 'stale' );
		if ( ! in_array( $filter, $valid_filters, true ) ) { $filter = 'all'; }

		// Hard cap to prevent runaway bulk runs.
		$hard_cap = (int) apply_filters( 'wplens/bulk_scan_cap', 500 );

		$args = array(
			'post_type'      => $types,
			'post_status'    => array( 'publish' ),
			'posts_per_page' => $hard_cap,
			'orderby'        => 'modified',
			'order'          => 'DESC',
			'fields'         => 'ids',
			'no_found_rows'  => true,
			'suppress_filters' => false,
		);

		$ids = get_posts( $args );

		// Apply scan-status filters.
		if ( 'unscanned' === $filter || 'stale' === $filter ) {
			global $wpdb;
			$scans_table = $wpdb->prefix . 'wplens_scans';
			$threshold   = gmdate( 'Y-m-d H:i:s', time() - 30 * DAY_IN_SECONDS );

			if ( ! empty( $ids ) ) {
				$placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );

				if ( 'unscanned' === $filter ) {
					// phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
					$scanned = $wpdb->get_col(
						$wpdb->prepare(
							"SELECT DISTINCT post_id FROM {$scans_table} WHERE post_id IN ({$placeholders})",
							...$ids
						)
					);
					$scanned = array_map( 'intval', (array) $scanned );
					$ids = array_values( array_diff( $ids, $scanned ) );
				} else {
					// 'stale' = scanned but not within 30 days.
					// phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
					$fresh = $wpdb->get_col(
						$wpdb->prepare(
							"SELECT DISTINCT post_id FROM {$scans_table} WHERE post_id IN ({$placeholders}) AND created_at >= %s",
							array_merge( $ids, array( $threshold ) )
						)
					);
					$fresh = array_map( 'intval', (array) $fresh );
					$ids = array_values( array_diff( $ids, $fresh ) );
				}
			}
		}

		// Hydrate a lightweight list for the JS layer.
		$items = array();
		foreach ( $ids as $id ) {
			$items[] = array(
				'id'        => (int) $id,
				'title'     => get_the_title( (int) $id ),
				'post_type' => get_post_type( (int) $id ),
			);
		}

		wp_send_json_success( array(
			'items'    => $items,
			'count'    => count( $items ),
			'hard_cap' => $hard_cap,
		) );
	}
}
