<?php
/**
 * NeccoSpeak — Logs tab (G5 audit / G31 analytics / G2 version-history
 * sub-nav) and Usage tab (G3 token usage + budget) admin screens.
 *
 * 2026-08 撤去記録: G19（AI応答への👍/👎評価）の設定セクション・
 * "feedback" サブビュー（neccospeak_logs_view_feedback()）・サブナビ項目・
 * dataset マップの項目は、無料版では提供しないという製品判断により削除した。
 * audit / analytics / versions の各ビューと admin_post_* ハンドラは無変更。
 *
 * 2026-08 撤去記録: G41（会話ログ）の 'conversation' データセット分岐・
 * CSV エクスポート機能（neccospeak_handle_logs_export() と CSV 整形3関数）は、
 * 会話ログ機能を無料版から提供しない製品判断により削除した。audit / analytics /
 * versions の削除・全削除ハンドラは無変更。
 *
 * Owner: impl-viewers (design phase5 §1.1). Public contract consumed by
 * includes/settings.php's tab dispatcher via function_exists() (design §1.3,
 * §5.5(b)):
 *   neccospeak_render_tab_logs( $opts )
 *   neccospeak_render_tab_usage( $opts )
 *
 * Both renderers only emit the *settings fields* for their tab (called
 * INSIDE the <form method="post" action="options.php"> that
 * includes/settings.php already opened, design §5.5(b) — never a nested
 * <form> here). The actual data-browsing UI (sub-nav, tables, delete/export
 * forms) is added via the 'logs_after_form' / 'usage_after_form' slots
 * (design §5.5(c)/§9.1/§9.3), which this file hooks itself with add_action()
 * so it needs no cooperating edit in includes/settings.php or
 * includes/admin-tabs.php.
 *
 * This file is the ONLY caller of includes/db.php's §3 read/write/delete API
 * for these datasets — it never touches $wpdb directly (design §15's "every
 * SQL statement originates in db.php" audit). Every db.php call is function_exists()-
 * guarded per the cross-agent contract (design §1.3) so this file degrades
 * to "data layer not available" notices rather than fataling when db.php
 * (impl-db) or the version-history view (impl-versions) have not landed yet.
 *
 * All admin_post_* handlers (design §9.1/§9.2/§9.3, INV-10) are split into a
 * `..._action()` function (auth + nonce check, does the work, returns a
 * small result array or calls wp_die()) and a thin `neccospeak_handle_...()`
 * wrapper that redirects + exit()s — the split exists purely so
 * tests/test-admin-logs.php can exercise the `..._action()` half without a
 * real `exit` tearing down the test process (this codebase's admin_post_*
 * handlers have never been unit-tested before Phase 5; there is no
 * `wp_safe_redirect()`-then-not-exit shim to lean on).
 *
 * Escaping convention (matches includes/admin-tabs.php): esc_html()/
 * esc_html__() for text nodes, esc_attr()/esc_attr__() for attributes,
 * esc_url( add_query_arg( …, admin_url( 'admin.php' ) ) ) for in-plugin
 * links. Conversation/audit body text always goes through
 * esc_html() inside a <pre> (design §9.1: "esc_html() 済みを
 * <pre class="neccospeak-log-pre"> に入れて改行を保つ") — never raw output —
 * so a stored `<script>` payload can never execute in wp-admin.
 *
 * @package NeccoSpeak
 */

defined( 'ABSPATH' ) || exit;

/* =========================================================================
 * Small shared helpers (URL building, GET-param readers, formatting).
 * ====================================================================== */

/**
 * Build a URL to this plugin's settings screen, always anchored on
 * admin_url('admin.php') (design §9.1 — matches the rest of the admin UI,
 * which is a single top-level page, never options-general.php).
 *
 * @param array $args Extra query args (e.g. 'tab', 'view', 'p').
 * @return string
 */
function neccospeak_logs_url( array $args = array() ) {
	$slug = defined( 'NECCOSPEAK_SETTINGS_SLUG' ) ? NECCOSPEAK_SETTINGS_SLUG : 'neccospeak';
	return add_query_arg( array_merge( array( 'page' => $slug ), $args ), admin_url( 'admin.php' ) );
}

/* =========================================================================
 * Logs tab — settings fields (design §4.3, 11 keys) + field-row helpers.
 * ====================================================================== */

/* =========================================================================
 * Logs tab — data-browsing sub-nav (design §9.1), hooked onto the
 * 'logs_after_form' slot (design §5.5(c)) so it renders after (and outside)
 * the settings <form> above.
 * ====================================================================== */

/**
 * Map a 'dataset' value (audit|event|version) to the
 * Logs sub-nav 'view' slug it belongs under, for post-action redirects.
 *
 * @param string $dataset
 * @return string
 */
function neccospeak_logs_dataset_view( $dataset ) {
	$map = array(
		'audit'   => 'audit',
		'event'   => 'analytics',
		'version' => 'versions',
	);
	return isset( $map[ $dataset ] ) ? $map[ $dataset ] : 'audit';
}

/* =========================================================================
 * Logs tab — admin_post_* handlers (design §9.1/§9.2, INV-10).
 * ====================================================================== */

/**
 * neccospeak_logs_delete / _delete_all's shared "row vs. whole session"
 * dispatch, testable without ever reaching wp_safe_redirect()+exit (see the
 * file header note on the action/handler split).
 *
 * @return array{view:string,notice:string}
 */
function neccospeak_logs_delete_action() {
	if ( ! current_user_can( 'manage_options' ) ) {
		wp_die( esc_html__( 'You are not allowed to do this.', 'neccospeak' ), '', array( 'response' => 403 ) );
	}
	check_admin_referer( 'neccospeak_logs_delete' );

	$dataset = isset( $_POST['dataset'] ) ? sanitize_key( wp_unslash( $_POST['dataset'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified above via check_admin_referer().
	$id      = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing

	// 2026-08 撤去記録: ここにあった 'conversation' の "whole session" 削除分岐
	// (dataset==='conversation' && session_id 指定時に neccospeak_db_delete_session()
	// を呼ぶ経路)は、会話ログ機能ごと削除した。$_POST['session_id'] の読み出しも
	// 併せて削除した(他に消費者なし)。
	$ok = false;
	if ( $id > 0 && in_array( $dataset, array( 'audit', 'event', 'version' ), true ) && function_exists( 'neccospeak_db_delete_row' ) ) {
		$ok = neccospeak_db_delete_row( $dataset, $id );
	}

	return array(
		'view'   => neccospeak_logs_dataset_view( $dataset ),
		'notice' => $ok ? 'deleted' : 'not_deleted',
	);
}

/**
 * admin_post_neccospeak_logs_delete wrapper: run the action, redirect, exit.
 *
 * @return void
 */
function neccospeak_handle_logs_delete() {
	$result = neccospeak_logs_delete_action();
	wp_safe_redirect(
		neccospeak_logs_url(
			array(
				'tab'                    => 'logs',
				'view'                   => $result['view'],
				'neccospeak_logs_notice' => $result['notice'],
			)
		)
	);
	exit;
}
add_action( 'admin_post_neccospeak_logs_delete', 'neccospeak_handle_logs_delete' );

/**
 * neccospeak_logs_delete_all's action half (design §9.1: requires typing
 * DELETE, INV-10).
 *
 * @return array{view:string,notice:string}
 */
function neccospeak_logs_delete_all_action() {
	if ( ! current_user_can( 'manage_options' ) ) {
		wp_die( esc_html__( 'You are not allowed to do this.', 'neccospeak' ), '', array( 'response' => 403 ) );
	}
	check_admin_referer( 'neccospeak_logs_delete_all' );

	$dataset = isset( $_POST['dataset'] ) ? sanitize_key( wp_unslash( $_POST['dataset'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing
	$confirm = isset( $_POST['confirm'] ) ? sanitize_text_field( wp_unslash( $_POST['confirm'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing

	$view = neccospeak_logs_dataset_view( $dataset );

	if ( 'DELETE' !== $confirm ) {
		return array(
			'view'   => $view,
			'notice' => 'confirm_required',
		);
	}

	$notice = 'invalid';
	if ( in_array( $dataset, array( 'audit', 'event' ), true ) && function_exists( 'neccospeak_db_delete_dataset' ) ) {
		neccospeak_db_delete_dataset( $dataset, array() );
		$notice = 'deleted_all';
	}

	return array(
		'view'   => $view,
		'notice' => $notice,
	);
}

/**
 * admin_post_neccospeak_logs_delete_all wrapper.
 *
 * @return void
 */
function neccospeak_handle_logs_delete_all() {
	$result = neccospeak_logs_delete_all_action();
	wp_safe_redirect(
		neccospeak_logs_url(
			array(
				'tab'                    => 'logs',
				'view'                   => $result['view'],
				'neccospeak_logs_notice' => $result['notice'],
			)
		)
	);
	exit;
}
add_action( 'admin_post_neccospeak_logs_delete_all', 'neccospeak_handle_logs_delete_all' );

// 2026-08 撤去記録: ここにあった会話ログ CSV エクスポート機能一式 —
// neccospeak_logs_conversation_csv_header() / neccospeak_csv_safe_cell()
// (CWE-1236 CSV式インジェクション対策) / neccospeak_logs_conversation_csv_row() /
// admin_post_neccospeak_logs_export ハンドラと add_action() — は、会話ログ
// 機能を無料版から提供しない製品判断により削除した。

/* =========================================================================
 * Usage tab — settings fields (design §4.2, 9 keys).
 * ====================================================================== */

/* =========================================================================
 * Usage tab — summary/gauge/tables, hooked onto the 'usage_after_form' slot.
 * ====================================================================== */

/* =========================================================================
 * Usage tab — admin_post_* handlers.
 * ====================================================================== */

/**
 * neccospeak_usage_reset's testable action half.
 *
 * @return string Notice key.
 */
function neccospeak_usage_reset_action() {
	if ( ! current_user_can( 'manage_options' ) ) {
		wp_die( esc_html__( 'You are not allowed to do this.', 'neccospeak' ), '', array( 'response' => 403 ) );
	}
	check_admin_referer( 'neccospeak_usage_reset' );

	$confirm = isset( $_POST['confirm'] ) ? sanitize_text_field( wp_unslash( $_POST['confirm'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified above via check_admin_referer().
	if ( 'DELETE' !== $confirm ) {
		return 'confirm_required';
	}
	if ( function_exists( 'neccospeak_db_reset_usage' ) ) {
		neccospeak_db_reset_usage();
	}
	return 'reset';
}

/**
 * admin_post_neccospeak_usage_reset wrapper.
 *
 * @return void
 */
function neccospeak_handle_usage_reset() {
	$notice = neccospeak_usage_reset_action();
	wp_safe_redirect(
		neccospeak_logs_url(
			array(
				'tab'                     => 'usage',
				'neccospeak_usage_notice' => $notice,
			)
		)
	);
	exit;
}
add_action( 'admin_post_neccospeak_usage_reset', 'neccospeak_handle_usage_reset' );

/**
 * neccospeak_usage_resume's testable action half (design §7.2/§7.3
 * "Resume now"): always resumes both chat and voice — a manual admin
 * override, unlike the automatic per-bucket pause the circuit breaker
 * itself applies.
 *
 * @return string Notice key.
 */
function neccospeak_usage_resume_action() {
	if ( ! current_user_can( 'manage_options' ) ) {
		wp_die( esc_html__( 'You are not allowed to do this.', 'neccospeak' ), '', array( 'response' => 403 ) );
	}
	check_admin_referer( 'neccospeak_usage_resume' );

	if ( function_exists( 'neccospeak_runtime_resume' ) ) {
		neccospeak_runtime_resume( 'all' );
	}
	if ( function_exists( 'neccospeak_db_log_audit' ) ) {
		neccospeak_db_log_audit(
			array(
				'name'    => 'resume',
				'subject' => defined( 'NECCOSPEAK_RUNTIME_OPTION' ) ? NECCOSPEAK_RUNTIME_OPTION : 'neccospeak_runtime',
				'user_id' => function_exists( 'get_current_user_id' ) ? (int) get_current_user_id() : 0,
				'changes' => array(),
				'note'    => 'manual resume from the Usage tab',
			)
		);
	}
	return 'resumed';
}

/**
 * admin_post_neccospeak_usage_resume wrapper.
 *
 * @return void
 */
function neccospeak_handle_usage_resume() {
	$notice = neccospeak_usage_resume_action();
	wp_safe_redirect(
		neccospeak_logs_url(
			array(
				'tab'                     => 'usage',
				'neccospeak_usage_notice' => $notice,
			)
		)
	);
	exit;
}
add_action( 'admin_post_neccospeak_usage_resume', 'neccospeak_handle_usage_resume' );

/* =========================================================================
 * Budget admin_notices banner (design §7.3, impl-viewers-owned).
 * ====================================================================== */

/**
 * Site-wide (every wp-admin screen, not just the Usage tab) warning/over
 * banner once the monthly budget crosses its warn threshold (design §7.3:
 * "警告/超過バナーは admin_notices にも出す").
 *
 * @return void
 */
function neccospeak_budget_admin_notice() {
	if ( ! current_user_can( 'manage_options' ) ) {
		return;
	}
	if ( ! function_exists( 'neccospeak_budget_state' ) ) {
		return;
	}
	$budget = neccospeak_budget_state();
	// A pause left over from before budget_enabled was turned off must still
	// surface here (review: "high" — the only other admin-facing pause
	// indicator, the Usage tab's own banner, was previously gated the same
	// way; see the fix in the Usage tab body above). $budget['paused_*'] comes
	// straight from neccospeak_runtime, unaffected by $budget['enabled'].
	$paused = ! empty( $budget['paused_chat'] ) || ! empty( $budget['paused_voice'] );
	if ( ! $paused && ( empty( $budget['enabled'] ) || 'ok' === $budget['level'] ) ) {
		return;
	}

	if ( $paused ) {
		$type = 'notice-error';
		$text = __( 'NeccoSpeak: chat and/or voice are currently paused by the usage budget circuit breaker.', 'neccospeak' );
	} else {
		$type = ( 'over' === $budget['level'] ) ? 'notice-error' : 'notice-warning';
		$text = ( 'over' === $budget['level'] )
			? __( 'NeccoSpeak: the monthly usage budget has been reached.', 'neccospeak' )
			: __( 'NeccoSpeak: the monthly usage budget is close to its limit.', 'neccospeak' );
	}
	$url = neccospeak_logs_url( array( 'tab' => 'usage' ) );

	echo '<div class="notice ' . esc_attr( $type ) . ' is-dismissible"><p>' . esc_html( $text ) . ' <a href="' . esc_url( $url ) . '">' . esc_html__( 'Open the Usage tab', 'neccospeak' ) . '</a></p></div>';
}
add_action( 'admin_notices', 'neccospeak_budget_admin_notice' );
