<?php
/**
 * Sync event recorder.
 *
 * @package SubmissionInbox
 */

namespace SUBIN\Sync;

use SUBIN\Mail\EmailErrorClassifier;

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

/**
 * Records local domain events for future Cloud synchronization.
 */
class SyncEventRecorder {
	/**
	 * Sync events.
	 *
	 * @var SyncEventRepository
	 */
	private $sync_events;

	/**
	 * Lead repository.
	 *
	 * @var \SUBIN\Leads\LeadRepository
	 */
	private $leads;

	/**
	 * Lead fields.
	 *
	 * @var \SUBIN\Leads\LeadFieldRepository
	 */
	private $fields;

	/**
	 * Lead activity.
	 *
	 * @var \SUBIN\Leads\LeadActivityRepository
	 */
	private $activity;

	/**
	 * Mail events.
	 *
	 * @var \SUBIN\MailLog\MailEventRepository
	 */
	private $mail_events;

	/**
	 * Settings repository.
	 *
	 * @var \SUBIN_SettingsRepository
	 */
	private $settings;

	/**
	 * Email error classifier.
	 *
	 * @var EmailErrorClassifier
	 */
	private $error_classifier;

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->sync_events      = new SyncEventRepository();
		$this->leads            = new \SUBIN\Leads\LeadRepository();
		$this->fields           = new \SUBIN\Leads\LeadFieldRepository();
		$this->activity         = new \SUBIN\Leads\LeadActivityRepository();
		$this->mail_events      = new \SUBIN\MailLog\MailEventRepository();
		$this->settings         = new \SUBIN_SettingsRepository();
		$this->error_classifier = new EmailErrorClassifier();
	}

	/**
	 * Register event hooks.
	 *
	 * @return void
	 */
	public function register() {
		add_action( 'subin_lead_created', array( $this, 'record_lead_created' ), 10, 1 );
		add_action( 'subin_lead_workflow_updated', array( $this, 'record_lead_workflow_updated' ), 10, 4 );
		add_action( 'subin_lead_note_added', array( $this, 'record_lead_note_added' ), 10, 3 );
		add_action( 'subin_lead_note_updated', array( $this, 'record_lead_note_updated' ), 10, 3 );
		add_action( 'subin_lead_note_deleted', array( $this, 'record_lead_note_deleted' ), 10, 3 );
		add_action( 'subin_mail_event_status_updated', array( $this, 'record_mail_event_status_updated' ), 10, 2 );
		add_action( 'subin_mail_event_no_email_generated', array( $this, 'record_no_email_generated' ), 10, 1 );
	}

	/**
	 * Record lead.created.
	 *
	 * @param int $lead_id Lead id.
	 * @return void
	 */
	public function record_lead_created( $lead_id ) {
		if ( ! $this->should_record() ) {
			return;
		}

		$payload = $this->lead_payload( $lead_id );
		if ( empty( $payload ) ) {
			return;
		}

		$this->sync_events->enqueue_lead_event( 'lead.created', $lead_id, $payload );
	}

	/**
	 * Record lead.workflow_updated.
	 *
	 * @param int                 $lead_id Lead id.
	 * @param array<string,mixed> $updates Updates.
	 * @param array<string,mixed> $changes Before/after change set.
	 * @param array<string,mixed> $workflow_snapshot Immutable committed workflow state.
	 * @return void
	 */
	public function record_lead_workflow_updated( $lead_id, array $updates, array $changes = array(), array $workflow_snapshot = array() ) {
		unset( $changes );
		if ( ! $this->should_record() ) {
			return;
		}
		$lead_payload = $this->apply_workflow_snapshot( $this->lead_payload( $lead_id ), $workflow_snapshot );
		if ( empty( $lead_payload ) ) {
			return;
		}
		unset( $lead_payload['notes'] );

		$this->sync_events->enqueue_lead_event(
			'lead.workflow_updated',
			$lead_id,
			array(
				'lead'    => $lead_payload,
				'updates' => $updates,
			)
		);
	}

	/**
	 * Record lead.note_added.
	 *
	 * @param int $lead_id Lead id.
	 * @param int $note_id Note id.
	 * @param array<string,mixed> $note_snapshot Immutable committed note state.
	 * @return void
	 */
	public function record_lead_note_added( $lead_id, $note_id, array $note_snapshot = array() ) {
		$this->record_lead_note_event( 'lead.note_added', $lead_id, $note_id, $note_snapshot );
	}

	/**
	 * Record lead.note_updated.
	 *
	 * @param int                 $lead_id Lead id.
	 * @param int                 $note_id Note id.
	 * @param array<string,mixed> $note_snapshot Immutable committed note state.
	 * @return void
	 */
	public function record_lead_note_updated( $lead_id, $note_id, array $note_snapshot = array() ) {
		$this->record_lead_note_event( 'lead.note_updated', $lead_id, $note_id, $note_snapshot );
	}

	/**
	 * Record lead.note_deleted.
	 *
	 * @param int                 $lead_id Lead id.
	 * @param int                 $note_id Note id.
	 * @param array<string,mixed> $note_snapshot Immutable committed tombstone.
	 * @return void
	 */
	public function record_lead_note_deleted( $lead_id, $note_id, array $note_snapshot = array() ) {
		$this->record_lead_note_event( 'lead.note_deleted', $lead_id, $note_id, $note_snapshot );
	}

	/**
	 * Record one immutable note mutation without re-reading aggregate note state.
	 *
	 * @param string              $event_type Note event type.
	 * @param int                 $lead_id Lead id.
	 * @param int                 $note_id Note id.
	 * @param array<string,mixed> $note_snapshot Immutable committed note state.
	 * @return void
	 */
	private function record_lead_note_event( $event_type, $lead_id, $note_id, array $note_snapshot ) {
		$event_type   = sanitize_key( str_replace( '.', '_', (string) $event_type ) );
		$event_types  = array(
			'lead_note_added'   => 'lead.note_added',
			'lead_note_updated' => 'lead.note_updated',
			'lead_note_deleted' => 'lead.note_deleted',
		);
		$lead_id      = absint( $lead_id );
		$note_id      = absint( $note_id );
		$note_version = absint( $note_snapshot['note_version'] ?? 0 );
		if (
			! isset( $event_types[ $event_type ] )
			|| $lead_id <= 0
			|| $note_id <= 0
			|| $note_id !== absint( $note_snapshot['id'] ?? 0 )
			|| $lead_id !== absint( $note_snapshot['lead_id'] ?? 0 )
			|| $note_version <= 0
		) {
			return;
		}

		$deleted      = ! empty( $note_snapshot['deleted'] );
		$should_record = $this->should_record();
		if ( ! $should_record && ! $deleted ) {
			return;
		}
		$lead_payload = $should_record ? $this->lead_payload( $lead_id ) : array( 'id' => $lead_id );
		if ( empty( $lead_payload ) ) {
			return;
		}
		unset( $lead_payload['notes'] );
		$note_payload = array(
			'id'           => $note_id,
			'note_version' => $note_version,
			'deleted'      => $deleted,
			'sync_footprint' => ! empty( $note_snapshot['sync_footprint'] ),
			'user_id'      => absint( $note_snapshot['user_id'] ?? 0 ),
			'created_at'   => sanitize_text_field( (string) ( $note_snapshot['created_at'] ?? '' ) ),
			'updated_at'   => sanitize_text_field( (string) ( $note_snapshot['updated_at'] ?? $note_snapshot['deleted_at'] ?? '' ) ),
		);
		if ( ! $deleted && 'full_lead_data' === $this->data_mode() ) {
			$note_payload['body'] = sanitize_textarea_field( (string) ( $note_snapshot['note'] ?? '' ) );
		}

		$this->sync_events->enqueue_note_event(
			$event_types[ $event_type ],
			$lead_id,
			array(
				'lead' => $lead_payload,
				'note' => $note_payload,
			),
			$should_record
		);
	}

	/**
	 * Record email status events.
	 *
	 * @param int    $event_id Event id.
	 * @param string $status Status.
	 * @return void
	 */
	public function record_mail_event_status_updated( $event_id, $status ) {
		if ( ! $this->should_record() ) {
			return;
		}

		$status = sanitize_key( (string) $status );
		if ( ! in_array( $status, array( 'sent', 'failed' ), true ) ) {
			return;
		}

		$this->sync_events->enqueue_mail_event(
			'email.' . $status,
			$event_id,
			$this->mail_event_payload( $event_id, $status )
		);
	}

	/**
	 * Record no-email-generated event.
	 *
	 * @param int $event_id Event id.
	 * @return void
	 */
	public function record_no_email_generated( $event_id ) {
		if ( ! $this->should_record() ) {
			return;
		}

		$this->sync_events->enqueue_mail_event(
			'email.no_email_generated',
			$event_id,
			$this->mail_event_payload( $event_id, 'no_email_generated' )
		);
	}

	/**
	 * Build mail event payload.
	 *
	 * @param int    $event_id Event id.
	 * @param string $status Status.
	 * @return array<string,mixed>
	 */
	private function mail_event_payload( $event_id, $status ) {
		$event = $this->mail_events->get( $event_id );
		if ( ! $event ) {
			return array(
				'event_id' => absint( $event_id ),
				'status'   => sanitize_key( $status ),
			);
		}

		$mode = $this->data_mode();
		$base = array(
			'event_id'      => absint( $event->id ?? $event_id ),
			'lead_id'       => absint( $event->lead_id ?? 0 ),
			'source'        => sanitize_key( $event->source ?? '' ),
			'status'        => sanitize_key( $status ),
			'error_message' => $this->error_classifier->sanitize_technical_error( (string) ( $event->error_message ?? '' ) ),
			'created_at'    => sanitize_text_field( $event->created_at ?? '' ),
		);

		if ( 'full_lead_data' !== $mode ) {
			$base['to_email_hash'] = $this->hash_value( $event->to_email ?? '' );
			$base['subject_hash']  = $this->hash_value( $event->subject ?? '' );

			return $base;
		}

		$base['to_email'] = sanitize_textarea_field( $event->to_email ?? '' );
		$base['subject']  = sanitize_text_field( $event->subject ?? '' );

		return $base;
	}

	/**
	 * Build full lead payload.
	 *
	 * @param int $lead_id Lead id.
	 * @return array<string,mixed>
	 */
	private function lead_payload( $lead_id ) {
		$lead = $this->leads->get( $lead_id );
		if ( ! $lead ) {
			return array();
		}

		$mode = $this->data_mode();
		$base = array(
			'id'              => absint( $lead->id ),
			'provider'        => sanitize_key( $lead->provider ?? '' ),
			'form_id'         => sanitize_text_field( $lead->form_id ?? '' ),
			'form_name'       => sanitize_text_field( $lead->form_name ?? '' ),
			'status'          => sanitize_key( $lead->status ?? '' ),
			'email_status'    => sanitize_key( $lead->email_status ?? '' ),
			'auto_reply_status' => sanitize_key( $lead->auto_reply_status ?? 'not_configured' ),
			'workflow_status' => sanitize_key( $lead->workflow_status ?? 'open' ),
			'priority'        => sanitize_key( $lead->priority ?? 'normal' ),
			'workflow_version' => max( 1, absint( $lead->workflow_version ?? 1 ) ),
			'submitted_at'    => sanitize_text_field( $lead->submitted_at ?? '' ),
		);

		if ( 'health_only' === $mode ) {
			return $base;
		}

		$summary = array_merge(
			$base,
			array(
				'customer_name'          => sanitize_text_field( $lead->customer_name ?? '' ),
				'customer_email_hash'    => $this->hash_value( $lead->customer_email ?? '' ),
				'customer_email_preview' => $this->email_preview( $lead->customer_email ?? '' ),
				'assigned_user_id' => absint( $lead->assigned_user_id ?? 0 ),
				'follow_up_at'     => sanitize_text_field( $lead->follow_up_at ?? '' ),
			)
		);

		if ( 'lead_summary' === $mode ) {
			return $summary;
		}

		$fields = array();
		foreach ( $this->fields->get_by_lead_id( $lead_id ) as $field ) {
			$is_sensitive = ! empty( $field->is_sensitive );
			$fields[] = array(
				'key'          => sanitize_text_field( $field->field_key ?? '' ),
				'label'        => sanitize_text_field( $field->field_label ?? '' ),
				'value'        => $is_sensitive ? '[redacted]' : sanitize_textarea_field( $field->field_value ?? '' ),
				'is_sensitive' => $is_sensitive,
			);
		}

		$notes = array();
		foreach ( $this->activity->get_notes( $lead_id ) as $note ) {
			$notes[] = array(
				'id'           => absint( $note->id ?? 0 ),
				'user_id'      => absint( $note->user_id ?? 0 ),
				'note'         => sanitize_textarea_field( $note->note ?? '' ),
				'note_version' => max( 1, absint( $note->note_version ?? 1 ) ),
				'created_at'   => sanitize_text_field( $note->created_at ?? '' ),
				'updated_at'   => sanitize_text_field( $note->updated_at ?? $note->created_at ?? '' ),
			);
		}

		return array_merge(
			$summary,
			array(
				'customer_email'  => sanitize_email( $lead->customer_email ?? '' ),
				'customer_phone'  => sanitize_text_field( $lead->customer_phone ?? '' ),
				'message_preview' => sanitize_textarea_field( $lead->message_preview ?? '' ),
				'source_url'      => esc_url_raw( $lead->source_url ?? '' ),
				'referrer'        => esc_url_raw( $lead->referrer ?? '' ),
				'fields'          => $fields,
				'notes'           => $notes,
			)
		);
	}

	/**
	 * Replace re-read workflow fields with the exact state committed by the writer.
	 *
	 * A second request can commit another version before this synchronous observer
	 * reads the lead. Applying the writer snapshot keeps every queued event
	 * internally consistent and ordered by workflow_version.
	 *
	 * @param array<string,mixed> $lead Lead payload built for the configured mode.
	 * @param array<string,mixed> $snapshot Committed workflow snapshot.
	 * @return array<string,mixed>
	 */
	private function apply_workflow_snapshot( array $lead, array $snapshot ) {
		if ( empty( $lead ) || empty( $snapshot ) ) {
			return $lead;
		}

		$lead['workflow_status']  = sanitize_key( $snapshot['workflow_status'] ?? $lead['workflow_status'] ?? 'open' );
		$lead['priority']         = sanitize_key( $snapshot['priority'] ?? $lead['priority'] ?? 'normal' );
		$lead['workflow_version'] = max( 1, absint( $snapshot['workflow_version'] ?? $lead['workflow_version'] ?? 1 ) );
		if ( array_key_exists( 'assigned_user_id', $lead ) ) {
			$lead['assigned_user_id'] = absint( $snapshot['assigned_user_id'] ?? $lead['assigned_user_id'] );
		}
		if ( array_key_exists( 'follow_up_at', $lead ) ) {
			$lead['follow_up_at'] = sanitize_text_field( $snapshot['follow_up_at'] ?? '' );
		}

		return $lead;
	}

	/**
	 * Whether local sync events should be recorded.
	 *
	 * @return bool
	 */
	private function should_record() {
		$settings = $this->settings->all();

		return ! empty( $settings['cloud_sync_enabled'] );
	}

	/**
	 * Configured local sync data mode.
	 *
	 * @return string
	 */
	private function data_mode() {
		$settings    = $this->settings->all();
		$modes       = array( 'health_only', 'lead_summary', 'full_lead_data' );
		$requested   = sanitize_key( (string) ( $settings['cloud_sync_data_mode'] ?? 'lead_summary' ) );
		$requested   = in_array( $requested, $modes, true ) ? $requested : 'lead_summary';
		$allowed_raw = sanitize_key( (string) ( $settings['cloud_sync_allowed_mode'] ?? '' ) );
		$allowed     = in_array( $allowed_raw, $modes, true ) ? $allowed_raw : $requested;
		$ranks       = array(
			'health_only'    => 1,
			'lead_summary'   => 2,
			'full_lead_data' => 3,
		);
		$mode = $ranks[ $requested ] <= $ranks[ $allowed ] ? $requested : $allowed;

		if ( 'full_lead_data' === $mode && ( empty( $settings['cloud_sync_full_data_consent'] ) || empty( $settings['cloud_sync_full_data_enabled'] ) ) ) {
			return 'lead_summary';
		}

		return $mode;
	}

	/**
	 * Hash a scalar value for non-full sync modes.
	 *
	 * @param mixed $value Value.
	 * @return string
	 */
	private function hash_value( $value ) {
		$value = strtolower( trim( (string) $value ) );

		return '' === $value ? '' : hash( 'sha256', $value );
	}

	/**
	 * Mask an email for summary sync mode.
	 *
	 * @param mixed $email Email.
	 * @return string
	 */
	private function email_preview( $email ) {
		$email = sanitize_email( (string) $email );
		if ( '' === $email || false === strpos( $email, '@' ) ) {
			return '';
		}

		list( $local, $domain ) = explode( '@', $email, 2 );

		return substr( $local, 0, 2 ) . '***@' . $domain;
	}
}
