<?php
/**
 * Activation routine: custom tables, capabilities, schema versioning.
 *
 * @package Leadite
 */

namespace Leadite;

defined( 'ABSPATH' ) || exit;

class Activator {

	/**
	 * Run on plugin activation.
	 */
	public static function activate() {
		self::maybe_migrate_from_leadhub();
		self::create_tables();
		self::add_capabilities();
		self::harden_upload_dir();
		self::check_upload_dir_privacy();
		update_option( 'leadite_db_version', \leadite_DB_VERSION );
		flush_rewrite_rules();
	}

	/**
	 * Seamlessly migrates data from LeadHub (v0.3.6d and below) to Leadite.
	 * Renames tables, moves options, and renames the uploads directory.
	 */
	public static function maybe_migrate_from_leadhub() {
		global $wpdb;

		// 1. Check if we have the old DB version option (signal of an old install).
		$old_db_version = get_option( 'leadhub_db_version' );
		if ( ! $old_db_version ) {
			return; // Nothing to migrate.
		}

		// 2. Rename tables if they exist and target names are free.
		$tables = array(
			'submissions',
			'meta',
			'notes',
			'log',
			'files',
			'tags',
			'submission_tags',
			'field_map',
		);

		foreach ( $tables as $table ) {
			$old_table = $wpdb->prefix . 'leadhub_' . $table;
			$new_table = $wpdb->prefix . 'leadite_' . $table;

			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange
			$exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $old_table ) );
			if ( $exists === $old_table ) {
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange
				$target_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $new_table ) );
				if ( $target_exists !== $new_table ) {
					// phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange,PluginCheck.Security.DirectDB
					$wpdb->query( "RENAME TABLE `{$old_table}` TO `{$new_table}`" );
				}
			}
		}

		// 3. Migrate Options.
		$old_settings = get_option( 'leadhub_settings' );
		if ( $old_settings && ! get_option( 'leadite_settings' ) ) {
			update_option( 'leadite_settings', $old_settings );
		}

		// 4. Migrate Uploads Folder.
		$upload_dir = wp_upload_dir();
		$old_dir    = $upload_dir['basedir'] . '/leadhub';
		$new_dir    = $upload_dir['basedir'] . '/leadite';

		if ( is_dir( $old_dir ) && ! is_dir( $new_dir ) ) {
			global $wp_filesystem;
			if ( ! $wp_filesystem ) {
				require_once ABSPATH . 'wp-admin/includes/file.php';
				WP_Filesystem();
			}
			if ( $wp_filesystem ) {
				$wp_filesystem->move( $old_dir, $new_dir );
			}
		}

		// 5. Cleanup old entries to avoid confusion.
		delete_option( 'leadhub_db_version' );
		delete_option( 'leadhub_settings' );

		// Clean up old transients if any.
		delete_transient( 'leadhub_v1_status_counts' );
		delete_transient( 'leadhub_v1_kpis' );
	}

	/**
	 * Write deny-all .htaccess, web.config, and silence index.php into uploads/Leadite/.
	 *
	 * Called on activation and on every DB-version upgrade so existing installs
	 * are hardened automatically. Safe to call multiple times (checks existence).
	 */
	public static function harden_upload_dir() {
		$upload_dir  = wp_upload_dir();
		$leadite_dir = $upload_dir['basedir'] . '/Leadite';

		if ( ! file_exists( $leadite_dir ) ) {
			wp_mkdir_p( $leadite_dir );
		}

		// Apache: deny direct HTTP access to every file in this directory tree.
		$htaccess = $leadite_dir . '/.htaccess';
		if ( ! file_exists( $htaccess ) ) {
			$rules = "# Block all direct HTTP access to lead attachments.\n"
				. "<IfModule mod_authz_core.c>\n"
				. "    Require all denied\n"
				. "</IfModule>\n"
				. "<IfModule !mod_authz_core.c>\n"
				. "    Order deny,allow\n"
				. "    Deny from all\n"
				. "</IfModule>\n";
			file_put_contents( $htaccess, $rules ); // phpcs:ignore WordPress.WP.AlternativeFunctions
		}

		// IIS: deny direct HTTP access (web.config).
		$webconfig = $leadite_dir . '/web.config';
		if ( ! file_exists( $webconfig ) ) {
			$wc_rules = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
				. "<configuration>\n"
				. "  <system.webServer>\n"
				. "    <security>\n"
				. "      <requestFiltering>\n"
				. "        <denyUrlSequences>\n"
				. "          <add sequence=\"Leadite/\" />\n"
				. "        </denyUrlSequences>\n"
				. "      </requestFiltering>\n"
				. "    </security>\n"
				. "    <staticContent>\n"
				. "      <remove fileExtension=\".*\" />\n"
				. "      <mimeMap fileExtension=\".*\" mimeType=\"application/octet-stream\" />\n"
				. "    </staticContent>\n"
				. "  </system.webServer>\n"
				. "</configuration>\n";
			file_put_contents( $webconfig, $wc_rules ); // phpcs:ignore WordPress.WP.AlternativeFunctions
		}

		// Silence file: prevents directory listing on servers that ignore .htaccess.
		$index = $leadite_dir . '/index.php';
		if ( ! file_exists( $index ) ) {
			file_put_contents( $index, '<?php // Silence is golden.' ); // phpcs:ignore WordPress.WP.AlternativeFunctions
		}
	}

	/**
	 * Best-effort self-test: check whether the server actually blocks direct HTTP
	 * access to files in uploads/Leadite/.
	 *
	 * Uses a random probe file and wp_remote_get() to verify the server returns
	 * 403 or 404. Saves the result as 'leadite_upload_protection_status':
	 *   'ok'      —— server correctly blocks direct access
	 *   'fail'    —— server returned 200 (file is publicly accessible)
	 *   'unknown' —— test could not be completed (firewall, Basic Auth, local dev, etc.)
	 *
	 * 'unknown' is NOT treated as an error —— it is only used to show a neutral notice.
	 * This method may be a no-op on local/non-HTTP environments.
	 *
	 * @return void
	 */
	public static function check_upload_dir_privacy() {
		$upload_dir  = wp_upload_dir();
		$leadite_dir = $upload_dir['basedir'] . '/Leadite';
		$leadite_URL = $upload_dir['baseurl'] . '/Leadite';

		// Cannot run test without a writable directory.
		if ( ! is_dir( $leadite_dir ) || ! is_writable( $leadite_dir ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable
			update_option( 'leadite_upload_protection_status', 'unknown' );
			return;
		}

		// Create a small, recognisable probe file.
		$probe_name = '_lh_probe_' . wp_generate_password( 12, false, false ) . '.txt';
		$probe_path = $leadite_dir . '/' . $probe_name;
		$probe_url  = $leadite_URL . '/' . $probe_name;

		// phpcs:ignore WordPress.WP.AlternativeFunctions
		if ( false === file_put_contents( $probe_path, 'Leadite-direct-access-probe' ) ) {
			update_option( 'leadite_upload_protection_status', 'unknown' );
			return;
		}

		// Non-blocking GET —— 5 s timeout is generous for a local file.
		$response = wp_remote_get(
			$probe_url,
			array(
				'timeout'     => 5,
				'redirection' => 0,
				'sslverify'   => apply_filters( 'https_local_ssl_verify', false ), // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- core WP filter name
			)
		);

		// Clean up the probe file immediately regardless of outcome.
		wp_delete_file( $probe_path );

		if ( is_wp_error( $response ) ) {
			// wp_remote_get itself failed (e.g. loopback blocked, DNS failure).
			// This is common on localhost/staging —— treat as unknown, not a failure.
			update_option( 'leadite_upload_protection_status', 'unknown' );
			return;
		}

		$code = (int) wp_remote_retrieve_response_code( $response );

		if ( 200 === $code ) {
			// Server returned the file —— protection is NOT working.
			update_option( 'leadite_upload_protection_status', 'fail' );
		} elseif ( $code >= 400 && $code < 600 ) {
			// Server denied the request —— protection is working correctly.
			update_option( 'leadite_upload_protection_status', 'ok' );
		} else {
			// Unexpected response (redirect, etc.) —— treat as unknown.
			update_option( 'leadite_upload_protection_status', 'unknown' );
		}
	}

	/**
	 * Create / migrate custom tables via dbDelta.
	 *
	 * Tables (utf8mb4 for Hebrew/Arabic/emoji):
	 *  - {prefix}leadite_submissions : one row per lead
	 *  - {prefix}leadite_meta        : EAV for dynamic form fields
	 *  - {prefix}leadite_notes       : manager notes
	 *  - {prefix}leadite_log         : audit + sync log
	 */
	public static function create_tables() {
		global $wpdb;

		require_once ABSPATH . 'wp-admin/includes/upgrade.php';

		$charset = $wpdb->get_charset_collate();
		$p       = $wpdb->prefix . 'leadite_';

		$submissions = "CREATE TABLE {$p}submissions (
			id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
			source VARCHAR(40) NOT NULL DEFAULT '',
			form_id VARCHAR(190) NOT NULL DEFAULT '',
			form_name VARCHAR(190) NOT NULL DEFAULT '',
			status VARCHAR(20) NOT NULL DEFAULT 'new',
			starred TINYINT(1) NOT NULL DEFAULT 0,
			spam_score TINYINT UNSIGNED NOT NULL DEFAULT 0,
			hash CHAR(32) NOT NULL DEFAULT '',
			ip VARCHAR(64) NOT NULL DEFAULT '',
			user_agent VARCHAR(255) NOT NULL DEFAULT '',
			referer VARCHAR(255) NOT NULL DEFAULT '',
			page_url VARCHAR(255) NOT NULL DEFAULT '',
			lang VARCHAR(12) NOT NULL DEFAULT '',
			assigned_to BIGINT UNSIGNED NOT NULL DEFAULT 0,
			created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
			read_at DATETIME NULL DEFAULT NULL,
			hot_score_cached SMALLINT NOT NULL DEFAULT 0,
			hot_score_version TINYINT UNSIGNED NOT NULL DEFAULT 0,
			sla_status VARCHAR(30) NOT NULL DEFAULT '',
			sla_due_at DATETIME NULL DEFAULT NULL,
			sla_checked_at DATETIME NULL DEFAULT NULL,
			is_returning TINYINT(1) NOT NULL DEFAULT 0,
			returning_count SMALLINT UNSIGNED NOT NULL DEFAULT 0,
			PRIMARY KEY  (id),
			KEY source (source),
			KEY form_id (form_id),
			KEY status (status),
			KEY created_at (created_at),
			KEY hash (hash),
			KEY read_at (read_at),
			KEY assigned_to (assigned_to),
			KEY sla_status (sla_status),
			KEY sla_due_at (sla_due_at),
			KEY hot_score_cached (hot_score_cached),
			KEY is_returning (is_returning)
		) {$charset};";

		$meta = "CREATE TABLE {$p}meta (
			id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
			submission_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
			field_key VARCHAR(190) NOT NULL DEFAULT '',
			field_label VARCHAR(190) NOT NULL DEFAULT '',
			field_value LONGTEXT NULL,
			field_type VARCHAR(40) NOT NULL DEFAULT 'text',
			canonical VARCHAR(40) NOT NULL DEFAULT '',
			sort_order INT NOT NULL DEFAULT 0,
			PRIMARY KEY  (id),
			KEY submission_id (submission_id),
			KEY canonical (canonical)
		) {$charset};";

		$notes = "CREATE TABLE {$p}notes (
			id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
			submission_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
			user_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
			note LONGTEXT NULL,
			created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
			PRIMARY KEY  (id),
			KEY submission_id (submission_id)
		) {$charset};";

		$log = "CREATE TABLE {$p}log (
			id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
			submission_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
			user_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
			action VARCHAR(40) NOT NULL DEFAULT '',
			detail VARCHAR(255) NOT NULL DEFAULT '',
			created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
			PRIMARY KEY  (id),
			KEY submission_id (submission_id),
			KEY action (action)
		) {$charset};";

		$files = "CREATE TABLE {$p}files (
			id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
			submission_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
			field_key VARCHAR(190) NOT NULL DEFAULT '',
			file_name VARCHAR(255) NOT NULL DEFAULT '',
			file_path VARCHAR(255) NOT NULL DEFAULT '',
			mime VARCHAR(100) NOT NULL DEFAULT '',
			size_bytes BIGINT UNSIGNED NOT NULL DEFAULT 0,
			skip_reason VARCHAR(500) NOT NULL DEFAULT '',
			PRIMARY KEY  (id),
			KEY submission_id (submission_id)
		) {$charset};";

		$tags = "CREATE TABLE {$p}tags (
			id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
			name VARCHAR(100) NOT NULL DEFAULT '',
			slug VARCHAR(100) NOT NULL DEFAULT '',
			color VARCHAR(20) NOT NULL DEFAULT '',
			PRIMARY KEY  (id),
			UNIQUE KEY slug (slug)
		) {$charset};";

		$submission_tags = "CREATE TABLE {$p}submission_tags (
			submission_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
			tag_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
			PRIMARY KEY  (submission_id, tag_id),
			KEY tag_id (tag_id)
		) {$charset};";

		$field_map = "CREATE TABLE {$p}field_map (
			id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
			source VARCHAR(40) NOT NULL DEFAULT '',
			form_id VARCHAR(190) NOT NULL DEFAULT '',
			field_key VARCHAR(190) NOT NULL DEFAULT '',
			canonical VARCHAR(40) NOT NULL DEFAULT '',
			PRIMARY KEY  (id),
			KEY source_form (source, form_id)
		) {$charset};";

		dbDelta( $submissions );
		dbDelta( $meta );
		dbDelta( $notes );
		dbDelta( $log );
		dbDelta( $files );
		dbDelta( $tags );
		dbDelta( $submission_tags );
		dbDelta( $field_map );

		// Harden the upload directory every time the schema is updated.
		self::harden_upload_dir();
	}

	/**
	 * Grant the manage_leads and view_leads capabilities to administrators.
	 *
	 * Called on activation and on every schema upgrade so the admin role is
	 * always up-to-date even after a plugin update without reactivation.
	 */
	public static function add_capabilities() {
		$role = get_role( 'administrator' );
		if ( $role ) {
			$role->add_cap( 'manage_leads' );
			$role->add_cap( 'view_leads' );
		}
	}

	/**
	 * Apply per-role access settings to WordPress role capabilities.
	 *
	 * Called whenever the Role Access section in Settings > General is saved.
	 * Iterates every registered WP role (except administrator) and:
	 *  - 'manage' → grants both manage_leads + view_leads
	 *  - 'view'   → grants view_leads only, removes manage_leads
	 *  - ''       → removes both capabilities
	 *
	 * @param array $role_access Map of role_slug => 'manage'|'view'|''.
	 * @return void
	 */
	public static function apply_role_access( array $role_access ) {
		global $wp_roles;

		if ( ! isset( $wp_roles ) ) {
			$wp_roles = new \WP_Roles();
		}

		foreach ( $wp_roles->role_names as $slug => $name ) {
			if ( 'administrator' === $slug ) {
				continue; // Administrator always has full access — never touch it here.
			}

			$role  = get_role( $slug );
			if ( ! $role ) {
				continue;
			}

			$level = isset( $role_access[ $slug ] ) ? $role_access[ $slug ] : '';

			switch ( $level ) {
				case 'manage':
					$role->add_cap( 'manage_leads' );
					$role->add_cap( 'view_leads' );
					break;
				case 'view':
					$role->remove_cap( 'manage_leads' );
					$role->add_cap( 'view_leads' );
					break;
				default:
					$role->remove_cap( 'manage_leads' );
					$role->remove_cap( 'view_leads' );
					break;
			}
		}
	}
}