<?php

declare(strict_types=1);

namespace SiteFort\Modules\Notifications;

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

use SiteFort\Includes\SecretManager;
use SiteFort\Includes\SettingsStore;
use SiteFort\Includes\Capabilities;

class NotificationSettings {

	private const SETTINGS_COLUMN = SettingsStore::COL_NOTIFICATIONS;
	private const MAX_COOLDOWN = 86400;
	private const LEGACY_WEBHOOK_SECRET = 'webhook_url';
	private const RECIPIENT_GROUP_PLUGIN_MANAGERS = 'group:sitefort-managers';

	private const PLUGIN_EMAIL_EVENTS = [
		'security.scan_completed',
		'security.vuln_detected',
		'security.firewall_block',
		'security.login_lockout',
		'security.2fa_change',
		'security.scan_failure',
		'security.tools_action',
		'security.admin_login',
		'security.plugin_deactivated',
		'security.login_url_changed',
	];

	private const SEVERITY_FILTERABLE_EVENTS = [
		'security.scan_completed',
		'security.vuln_detected',
	];

	private const COOLDOWN_EVENTS = [
		'security.login_lockout',
		'security.admin_login',
	];

	private const DIGEST_SCHEDULABLE_EVENTS = [
		'security.firewall_block',
	];

	private const VALID_SEVERITY_THRESHOLDS = [ 'all', 'low', 'medium', 'high', 'critical' ];
	private const VALID_DIGEST_SCHEDULES    = [ 'daily', 'weekly', 'monthly' ];
	private const VALID_WEBHOOK_PROVIDERS   = [ 'slack', 'discord', 'generic' ];

	private function defaults(): array {
		return [
			'email_enabled'            => true,
			'recipient_email'          => '',
			'recipient_admin_contacts' => [],
			'events'                   => [
				'security.scan_completed'     => true,
				'security.vuln_detected'      => true,
				'security.firewall_block'     => false,
				'security.login_lockout'      => true,
				'security.2fa_change'         => false,
				'security.scan_failure'       => false,
				'security.tools_action'       => false,
				'security.admin_login'        => true,
				'security.plugin_deactivated' => true,
				'security.login_url_changed'  => true,
			],
			'webhook_enabled'          => false,
			'webhook_active_provider'  => 'slack',
			'cooldowns'                => [
				'security.login_lockout'  => 3600,
				'security.admin_login'    => 300,
			],
			'severity_thresholds'      => [
				'security.scan_completed' => 'medium',
				'security.vuln_detected'  => 'medium',
			],
			'digest_schedules'         => [
				'security.firewall_block' => 'weekly',
			],
		];
	}

	public function get_storage_defaults(): array {
		return $this->defaults();
	}

	/**
	 * @param mixed $value
	 * @return array<int,string>
	 */
	private static function split_recipient_email_tokens( $value ): array {
		$fragments = [];

		if ( is_array( $value ) ) {
			array_walk_recursive(
				$value,
				static function ( $item ) use ( &$fragments ): void {
					if ( is_scalar( $item ) ) {
						$fragments[] = (string) $item;
					}
				}
			);
		} elseif ( is_scalar( $value ) ) {
			$fragments[] = (string) $value;
		}

		$tokens = [];
		foreach ( $fragments as $fragment ) {
			$parts = preg_split( '/[\s,;]+/', (string) $fragment ) ?: [];
			foreach ( $parts as $part ) {
				$part = trim( (string) $part );
				if ( '' !== $part ) {
					$tokens[] = $part;
				}
			}
		}

		return $tokens;
	}

	private static function normalize_email_address( string $email ): string {
		return strtolower( sanitize_email( trim( $email ) ) );
	}

	/**
	 * @param mixed $value
	 */
	public static function normalize_recipient_email_string( $value ): string {
		$emails = [];

		foreach ( self::split_recipient_email_tokens( $value ) as $candidate ) {
			$email = self::normalize_email_address( $candidate );
			if ( '' !== $email && is_email( $email ) ) {
				$emails[] = $email;
			}
		}

		$emails = array_values( array_unique( $emails ) );
		sort( $emails, SORT_STRING );

		return implode( ', ', $emails );
	}

	/**
	 * @param mixed $value
	 */
	public static function validate_recipient_email_string( $value ): string {
		$emails  = [];
		$invalid = [];

		foreach ( self::split_recipient_email_tokens( $value ) as $candidate ) {
			$email = self::normalize_email_address( $candidate );
			if ( '' !== $email && is_email( $email ) ) {
				$emails[] = $email;
				continue;
			}

			$invalid[] = sanitize_text_field( $candidate );
		}

		if ( ! empty( $invalid ) ) {
			$invalid_preview = array_values( array_unique( $invalid ) );
			$invalid_preview = array_slice( $invalid_preview, 0, 5 );

			throw new \InvalidArgumentException(
				sprintf(
					/* translators: %s: comma separated invalid email addresses. */
					esc_html__( 'Invalid notification recipient email(s): %s', 'sitefort' ),
					esc_html( implode( ', ', $invalid_preview ) )
				)
			);
		}

		$emails = array_values( array_unique( $emails ) );
		sort( $emails, SORT_STRING );

		return implode( ', ', $emails );
	}

	/**
	 * @param mixed $contacts
	 * @return array<int,string>
	 */
	public static function sanitize_recipient_admin_contacts( $contacts ): array {
		$values = [];

		if ( is_array( $contacts ) ) {
			array_walk_recursive(
				$contacts,
				static function ( $item ) use ( &$values ): void {
					if ( is_scalar( $item ) ) {
						$values[] = (string) $item;
					}
				}
			);
		} elseif ( is_scalar( $contacts ) ) {
			$values[] = (string) $contacts;
		}

		$sanitized = [];
		foreach ( $values as $value ) {
			$value = trim( $value );
			if ( '' === $value ) {
				continue;
			}

			if ( self::RECIPIENT_GROUP_PLUGIN_MANAGERS === $value ) {
				$sanitized[] = $value;
				continue;
			}

			$email = self::normalize_email_address( $value );
			if ( '' !== $email && is_email( $email ) ) {
				$sanitized[] = $email;
			}
		}

		$sanitized = array_values( array_unique( $sanitized ) );
		sort( $sanitized, SORT_STRING );

		return $sanitized;
	}

	private function normalize_provider( string $provider ): string {
		return in_array( $provider, self::VALID_WEBHOOK_PROVIDERS, true ) ? $provider : 'slack';
	}

	private function get_provider_secret_name( string $provider ): string {
		return 'webhook_url_' . $this->normalize_provider( $provider );
	}

	/**
	 * Build a configured-state map for all supported webhook providers.
	 *
	 * @return array<string,bool>
	 */
	private function get_configured_provider_map(): array {
		$configured = [];
		foreach ( self::VALID_WEBHOOK_PROVIDERS as $provider ) {
			$configured[ $provider ] = '' !== $this->read_provider_webhook_url( $provider );
		}

		return $configured;
	}

	/**
	 * Resolve the selected delivery provider against currently saved webhook URLs.
	 */
	private function resolve_selected_provider( string $provider, array $configured_map ): string {
		$provider = $this->normalize_provider( $provider );
		if ( ! empty( $configured_map[ $provider ] ) ) {
			return $provider;
		}

		foreach ( self::VALID_WEBHOOK_PROVIDERS as $candidate ) {
			if ( ! empty( $configured_map[ $candidate ] ) ) {
				return $candidate;
			}
		}

		return $provider;
	}

	private function read_provider_webhook_url( string $provider ): string {
		$provider = $this->normalize_provider( $provider );
		$url      = SecretManager::get( $this->get_provider_secret_name( $provider ) );

		if ( ! is_string( $url ) ) {
			return '';
		}

		$url = trim( $url );
		if ( '' === $url ) {
			return '';
		}

		return WebhookDelivery::validate_webhook_url( $provider, $url ) ? $url : '';
	}

	private function build_storage_payload( array $saved, array $defaults ): array {
		$saved_cooldowns = array_intersect_key(
			(array) ( $saved['cooldowns'] ?? [] ),
			array_flip( self::COOLDOWN_EVENTS )
		);

		return [
			'email_enabled'            => (bool) ( $saved['email_enabled'] ?? $defaults['email_enabled'] ),
			'recipient_email'          => self::normalize_recipient_email_string( $saved['recipient_email'] ?? $defaults['recipient_email'] ),
			'recipient_admin_contacts' => self::sanitize_recipient_admin_contacts( $saved['recipient_admin_contacts'] ?? $defaults['recipient_admin_contacts'] ),
			'events'                   => array_merge( $defaults['events'], (array) ( $saved['events'] ?? [] ) ),
			'webhook_enabled'          => (bool) ( $saved['webhook_enabled'] ?? $defaults['webhook_enabled'] ),
			'webhook_active_provider'  => $this->normalize_provider(
				(string) ( $saved['webhook_active_provider'] ?? $saved['webhook_provider'] ?? $defaults['webhook_active_provider'] )
			),
			'cooldowns'                => array_merge( $defaults['cooldowns'], $saved_cooldowns ),
			'severity_thresholds'      => array_merge( $defaults['severity_thresholds'], (array) ( $saved['severity_thresholds'] ?? [] ) ),
			'digest_schedules'         => array_merge( $defaults['digest_schedules'], (array) ( $saved['digest_schedules'] ?? [] ) ),
		];
	}

	private function maybe_migrate_legacy_webhook_secret( array $saved, array $defaults ): array {
		$legacy_url = SecretManager::get( self::LEGACY_WEBHOOK_SECRET );
		$legacy_url = is_string( $legacy_url ) ? trim( $legacy_url ) : '';

		$has_new_secret = false;
		foreach ( self::VALID_WEBHOOK_PROVIDERS as $provider ) {
			if ( '' !== $this->read_provider_webhook_url( $provider ) ) {
				$has_new_secret = true;
				break;
			}
		}

		$has_legacy_keys = isset( $saved['webhook_url'] ) || isset( $saved['webhook_provider'] );
		if ( '' === $legacy_url && ! $has_legacy_keys ) {
			return $saved;
		}

		$provider = $this->normalize_provider(
			(string) ( $saved['webhook_active_provider'] ?? $saved['webhook_provider'] ?? $defaults['webhook_active_provider'] )
		);

		if ( '' !== $legacy_url && ! $has_new_secret ) {
			SecretManager::set( $this->get_provider_secret_name( $provider ), $legacy_url );
		}

		SecretManager::delete( self::LEGACY_WEBHOOK_SECRET );

		unset( $saved['webhook_url'], $saved['webhook_provider'] );
		$saved['webhook_active_provider'] = $provider;

		$normalized = $this->build_storage_payload( $saved, $defaults );
		SettingsStore::save( self::SETTINGS_COLUMN, $normalized );

		return $normalized;
	}

	public function get_all(): array {
		$defaults = $this->defaults();
		$saved    = SettingsStore::get( self::SETTINGS_COLUMN );
		$saved    = is_array( $saved ) ? $saved : [];
		$saved    = $this->maybe_migrate_legacy_webhook_secret( $saved, $defaults );
		$settings = $this->build_storage_payload( $saved, $defaults );

		$configured_map  = $this->get_configured_provider_map();
		$providers       = [];
		$any_configured  = in_array( true, $configured_map, true );
		$active_provider = $this->resolve_selected_provider( (string) $settings['webhook_active_provider'], $configured_map );
		$webhook_enabled = (bool) $settings['webhook_enabled'] && ! empty( $configured_map[ $active_provider ] );

		$settings['webhook_active_provider'] = $active_provider;
		$settings['webhook_enabled']         = $webhook_enabled;

		foreach ( self::VALID_WEBHOOK_PROVIDERS as $provider ) {
			$configured = ! empty( $configured_map[ $provider ] );
			$providers[ $provider ] = [
				'configured' => $configured,
				'selected'   => ( $provider === $active_provider ),
				'live'       => ( $webhook_enabled && $provider === $active_provider && $configured ),
				'active'     => ( $provider === $active_provider ),
			];
		}

		$settings['webhook_providers']                = $providers;
		$settings['webhook_configured']               = $any_configured;
		$settings['webhook_active_provider_configured'] = ! empty( $providers[ $active_provider ]['configured'] );

		return $settings;
	}

	public function get_redacted( bool $include_webhook_configured = false ): array {
		return $this->get_all();
	}

	public function get_provider_webhook_url( string $provider ): string {
		return $this->read_provider_webhook_url( $provider );
	}

	public function has_provider_webhook_url( string $provider ): bool {
		return '' !== $this->get_provider_webhook_url( $provider );
	}

	public function resolve_active_webhook(): array {
		$settings = $this->get_all();
		$provider = $this->normalize_provider( (string) ( $settings['webhook_active_provider'] ?? 'slack' ) );

		return [
			'provider' => $provider,
			'url'      => $this->get_provider_webhook_url( $provider ),
		];
	}

	public function save( array $input ): void {
		$defaults = $this->defaults();
		$current  = SettingsStore::get( self::SETTINGS_COLUMN );
		$current  = is_array( $current ) ? $current : [];
		$current  = $this->maybe_migrate_legacy_webhook_secret( $current, $defaults );
		$current  = $this->build_storage_payload( $current, $defaults );

		if ( isset( $input['email_enabled'] ) ) {
			$current['email_enabled'] = (bool) $input['email_enabled'];
		}

		if ( isset( $input['recipient_email'] ) ) {
			$current['recipient_email'] = self::validate_recipient_email_string( $input['recipient_email'] );
		}

		if ( isset( $input['recipient_admin_contacts'] ) ) {
			$current['recipient_admin_contacts'] = self::sanitize_recipient_admin_contacts( $input['recipient_admin_contacts'] );
		}

		if ( isset( $input['webhook_enabled'] ) ) {
			$current['webhook_enabled'] = (bool) $input['webhook_enabled'];
		}

		if ( isset( $input['webhook_active_provider'] ) ) {
			$current['webhook_active_provider'] = $this->normalize_provider( (string) $input['webhook_active_provider'] );
		}

		if ( isset( $input['webhook_provider_urls'] ) && is_array( $input['webhook_provider_urls'] ) ) {
			foreach ( $input['webhook_provider_urls'] as $provider => $raw_url ) {
				if ( ! is_scalar( $provider ) || ! in_array( (string) $provider, self::VALID_WEBHOOK_PROVIDERS, true ) ) {
					continue;
				}

				$normalized_provider = (string) $provider;
				$validated_url       = trim( esc_url_raw( is_scalar( $raw_url ) ? (string) $raw_url : '' ) );

				if ( '' !== $validated_url && ! WebhookDelivery::validate_webhook_url( $normalized_provider, $validated_url ) ) {
					throw new \InvalidArgumentException(
						sprintf(
							/* translators: %s: webhook provider name. */
							esc_html__( 'Invalid %s webhook URL format.', 'sitefort' ),
							esc_html( ucfirst( sanitize_text_field( $normalized_provider ) ) )
						)
					);
				}

				if ( '' !== $validated_url ) {
					SecretManager::set( $this->get_provider_secret_name( $normalized_provider ), $validated_url );
				} else {
					SecretManager::delete( $this->get_provider_secret_name( $normalized_provider ) );
				}
			}
		}

		$configured_map = $this->get_configured_provider_map();
		$current['webhook_active_provider'] = $this->resolve_selected_provider(
			(string) $current['webhook_active_provider'],
			$configured_map
		);

		if ( isset( $input['events'] ) && is_array( $input['events'] ) ) {
			foreach ( $input['events'] as $key => $enabled ) {
				if ( in_array( $key, self::PLUGIN_EMAIL_EVENTS, true ) ) {
					$current['events'][ $key ] = (bool) $enabled;
				}
			}
		}

		if ( isset( $input['cooldowns'] ) && is_array( $input['cooldowns'] ) ) {
			foreach ( $input['cooldowns'] as $key => $seconds ) {
				if ( in_array( $key, self::COOLDOWN_EVENTS, true ) ) {
					$current['cooldowns'][ $key ] = min( absint( $seconds ), self::MAX_COOLDOWN );
				}
			}
		}

		if ( isset( $input['severity_thresholds'] ) && is_array( $input['severity_thresholds'] ) ) {
			foreach ( $input['severity_thresholds'] as $key => $threshold ) {
				if ( in_array( $key, self::SEVERITY_FILTERABLE_EVENTS, true )
					&& in_array( (string) $threshold, self::VALID_SEVERITY_THRESHOLDS, true )
				) {
					$current['severity_thresholds'][ $key ] = (string) $threshold;
				}
			}
		}

		if ( isset( $input['digest_schedules'] ) && is_array( $input['digest_schedules'] ) ) {
			foreach ( $input['digest_schedules'] as $key => $schedule ) {
				if ( in_array( $key, self::DIGEST_SCHEDULABLE_EVENTS, true )
					&& in_array( (string) $schedule, self::VALID_DIGEST_SCHEDULES, true )
				) {
					$current['digest_schedules'][ $key ] = (string) $schedule;
				}
			}
		}

		if ( $current['webhook_enabled'] && empty( $configured_map[ $current['webhook_active_provider'] ] ) ) {
			throw new \InvalidArgumentException( esc_html__( 'Save a webhook URL for the active provider before enabling webhook notifications.', 'sitefort' ) );
		}

		SettingsStore::save( self::SETTINGS_COLUMN, $this->build_storage_payload( $current, $defaults ) );
		do_action( 'sitefort_settings_saved_notifications', $this->get_redacted( true ) );
	}

	/**
	 * Settings derived only from the stored column (events, email, recipients,
	 * schedules). Unlike get_all() this reads no webhook secrets and runs no
	 * legacy migration, so flag checks on the front end stay query-free.
	 *
	 * @return array<string,mixed>
	 */
	private function get_base_settings(): array {
		$defaults = $this->defaults();
		$saved    = SettingsStore::get( self::SETTINGS_COLUMN );

		return $this->build_storage_payload( is_array( $saved ) ? $saved : [], $defaults );
	}

	public function is_event_enabled( string $event_type ): bool {
		$settings = $this->get_base_settings();
		return (bool) ( $settings['events'][ $event_type ] ?? false );
	}

	public function is_email_enabled(): bool {
		$settings = $this->get_base_settings();
		return (bool) ( $settings['email_enabled'] ?? false );
	}

	/**
	 * @return array<int,string>
	 */
	public function get_recipients(): array {
		$settings   = $this->get_base_settings();
		$recipients = array_merge(
			self::split_recipient_email_tokens( (string) ( $settings['recipient_email'] ?? '' ) ),
			$this->resolve_admin_contact_emails( (array) ( $settings['recipient_admin_contacts'] ?? [] ) )
		);

		$normalized = [];
		foreach ( $recipients as $recipient ) {
			$email = self::normalize_email_address( (string) $recipient );
			if ( '' !== $email && is_email( $email ) ) {
				$normalized[] = $email;
			}
		}

		$normalized = array_values( array_unique( $normalized ) );
		if ( ! empty( $normalized ) ) {
			return $normalized;
		}

		$admin_email = $this->get_fallback_recipient_email();
		if ( '' !== $admin_email ) {
			return [ $admin_email ];
		}

		return [];
	}

	public function get_fallback_recipient_email(): string {
		$admin_email = self::normalize_email_address( (string) get_option( 'admin_email' ) );
		return '' !== $admin_email && is_email( $admin_email ) ? $admin_email : '';
	}

	/**
	 * Legacy string accessor retained for backward compatibility.
	 */
	public function get_recipient(): string {
		return implode( ', ', $this->get_recipients() );
	}

	/**
	 * @return array{groups: array<int,array<string,string>>, users: array<int,array<string,string>>}
	 */
	public function get_available_admin_contacts(): array {
		$group_label = is_multisite()
			? __( 'All Network Administrators', 'sitefort' )
			: __( 'All "Administrator" Users', 'sitefort' );
		$group_description = is_multisite()
			? __( 'Send notifications to every current network administrator with SiteFort access.', 'sitefort' )
			: __( 'Send notifications to every current WordPress administrator account.', 'sitefort' );

		$users       = [];
		$seen_emails = [];
		foreach ( $this->get_manage_plugin_users() as $user ) {
			$email = self::normalize_email_address( (string) $user->user_email );
			if ( '' === $email || ! is_email( $email ) ) {
				continue;
			}

			if ( isset( $seen_emails[ $email ] ) ) {
				continue;
			}
			$seen_emails[ $email ] = true;

			$display = trim( (string) $user->display_name );
			$login   = trim( (string) $user->user_login );
			$label   = '' !== $display ? $display : $login;

			if ( '' !== $display && '' !== $login && strcasecmp( $display, $login ) !== 0 ) {
				$label .= ' (' . $login . ')';
			} elseif ( '' === $label ) {
				$label = $email;
			}

			$users[] = [
				'value' => $email,
				'label' => $label,
				'email' => $email,
			];
		}

		return [
			'groups' => [
				[
					'value'       => self::RECIPIENT_GROUP_PLUGIN_MANAGERS,
					'label'       => $group_label,
					'description' => $group_description,
				],
			],
			'users'  => $users,
		];
	}

	public function get_cooldown( string $event_type ): int {
		$settings = $this->get_all();
		return (int) ( $settings['cooldowns'][ $event_type ] ?? 0 );
	}

	public function get_severity_threshold( string $event_type ): string {
		$settings = $this->get_all();
		return (string) ( $settings['severity_thresholds'][ $event_type ] ?? 'all' );
	}

	public function get_digest_schedule( string $event_type ): string {
		$settings = $this->get_all();
		$schedule = (string) ( $settings['digest_schedules'][ $event_type ] ?? 'daily' );
		return in_array( $schedule, self::VALID_DIGEST_SCHEDULES, true ) ? $schedule : 'daily';
	}

	/**
	 * @param array<int,string> $contacts
	 * @return array<int,string>
	 */
	private function resolve_admin_contact_emails( array $contacts ): array {
		$emails = [];

		foreach ( self::sanitize_recipient_admin_contacts( $contacts ) as $contact ) {
			if ( self::RECIPIENT_GROUP_PLUGIN_MANAGERS === $contact ) {
				$emails = array_merge( $emails, $this->get_plugin_manager_group_emails() );
				continue;
			}

			$emails[] = $contact;
		}

		return array_values( array_unique( $emails ) );
	}

	/**
	 * @return array<int,string>
	 */
	private function get_plugin_manager_group_emails(): array {
		$emails = [];

		if ( is_multisite() ) {
			foreach ( (array) get_super_admins() as $login ) {
				$user = get_user_by( 'login', (string) $login );
				if ( ! $user instanceof \WP_User ) {
					continue;
				}

				$email = self::normalize_email_address( (string) $user->user_email );
				if ( '' !== $email && is_email( $email ) ) {
					$emails[] = $email;
				}
			}

			return array_values( array_unique( $emails ) );
		}

		$admins = get_users(
			[
				'role'   => 'administrator',
				'fields' => [ 'user_email' ],
			]
		);

		foreach ( $admins as $admin ) {
			$email = '';
			if ( is_object( $admin ) && isset( $admin->user_email ) ) {
				$email = self::normalize_email_address( (string) $admin->user_email );
			}

			if ( '' !== $email && is_email( $email ) ) {
				$emails[] = $email;
			}
		}

		return array_values( array_unique( $emails ) );
	}

	/**
	 * @return array<int,\WP_User>
	 */
	private function get_manage_plugin_users(): array {
		$users = [];

		if ( is_multisite() ) {
			foreach ( (array) get_super_admins() as $login ) {
				$user = get_user_by( 'login', (string) $login );
				if ( $user instanceof \WP_User && Capabilities::user_can_manage_plugin( $user ) ) {
					$users[ $user->ID ] = $user;
				}
			}
		} else {
			foreach ( get_users() as $user ) {
				if ( $user instanceof \WP_User && Capabilities::user_can_manage_plugin( $user ) ) {
					$users[ $user->ID ] = $user;
				}
			}
		}

		$users = array_values( $users );
		usort(
			$users,
			static function ( \WP_User $left, \WP_User $right ): int {
				$left_label  = '' !== trim( (string) $left->display_name ) ? (string) $left->display_name : (string) $left->user_login;
				$right_label = '' !== trim( (string) $right->display_name ) ? (string) $right->display_name : (string) $right->user_login;

				return strcasecmp( $left_label, $right_label );
			}
		);

		return $users;
	}
}
