<?php
/**
 * Contact Service
 *
 * Handles saving and managing user contacts (email, phone, name).
 *
 * @package Niwa_AI
 * @since   0.2.0
 */

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

class Niwa_AI_Contact_Service {

	/**
	 * Database table name (without prefix).
	 *
	 * @var string
	 */
	const TABLE_NAME = 'niwa_ai_contacts';
	const SCHEMA_OPTION = 'niwa_ai_contact_schema_stamp';
	const SCHEMA_STAMP  = 'contacts-v2';

	/**
	 * Get the full table name with prefix.
	 *
	 * @return string
	 */
	private function get_table_name() {
		global $wpdb;
		return $wpdb->prefix . self::TABLE_NAME;
	}

	/**
	 * Ensure the contacts table and the CRM context columns exist.
	 *
	 * dbDelta is intentionally backed by explicit column checks here. Older
	 * installations created the contacts table before details, summaries, and
	 * updated_at existed, and CREATE TABLE IF NOT EXISTS prevented those sites
	 * from being upgraded reliably.
	 *
	 * @param bool $force Re-run the schema verification even when stamped.
	 * @return bool Whether every required CRM column is available.
	 */
	public function ensure_table_schema( $force = false ) {
		if ( ! $force && self::SCHEMA_STAMP === get_option( self::SCHEMA_OPTION, '' ) ) {
			return true;
		}

		global $wpdb;

		$table_name      = $this->get_table_name();
		$charset_collate = $wpdb->get_charset_collate();

		$sql = "CREATE TABLE {$table_name} (
			id mediumint(9) NOT NULL AUTO_INCREMENT,
			email varchar(255) NOT NULL DEFAULT '',
			name varchar(255) NOT NULL DEFAULT '',
			phone varchar(50) NOT NULL DEFAULT '',
			details longtext NULL,
			conversation_summary longtext NULL,
			created_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
			updated_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
			consent_given tinyint(1) DEFAULT 1 NOT NULL,
			consent_source varchar(50) DEFAULT 'chat' NOT NULL,
			consent_timestamp datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
			PRIMARY KEY  (id),
			UNIQUE KEY email (email)
		) {$charset_collate};";

		require_once ABSPATH . 'wp-admin/includes/upgrade.php';
		dbDelta( $sql );

		$columns = $this->get_table_columns();
		if ( ! isset( $columns['details'] ) ) {
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$wpdb->query( "ALTER TABLE {$table_name} ADD COLUMN details longtext NULL AFTER phone" );
		}
		$columns = $this->get_table_columns();
		if ( ! isset( $columns['conversation_summary'] ) ) {
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$wpdb->query( "ALTER TABLE {$table_name} ADD COLUMN conversation_summary longtext NULL AFTER details" );
		}
		$columns = $this->get_table_columns();
		if ( ! isset( $columns['updated_at'] ) ) {
			// Add as nullable first so existing rows can be backfilled safely.
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$wpdb->query( "ALTER TABLE {$table_name} ADD COLUMN updated_at datetime NULL DEFAULT NULL AFTER created_at" );
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$wpdb->query( "UPDATE {$table_name} SET updated_at = created_at WHERE updated_at IS NULL" );
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$wpdb->query( "ALTER TABLE {$table_name} MODIFY COLUMN updated_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL" );
		}

		$columns = $this->get_table_columns();
		$ready   = isset( $columns['id'], $columns['email'], $columns['details'], $columns['conversation_summary'], $columns['updated_at'] );
		if ( $ready ) {
			update_option( self::SCHEMA_OPTION, self::SCHEMA_STAMP, false );
		}

		return $ready;
	}

	/**
	 * Read the current contact table columns keyed by lowercase name.
	 *
	 * @return array
	 */
	private function get_table_columns() {
		global $wpdb;
		$table_name = $this->get_table_name();
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$names = $wpdb->get_col( "SHOW COLUMNS FROM {$table_name}", 0 );
		$out   = array();
		foreach ( (array) $names as $name ) {
			$out[ strtolower( (string) $name ) ] = true;
		}
		return $out;
	}

	/**
	 * Save a contact to the database.
	 *
	 * @param string $email   User email.
	 * @param string $name    User name.
	 * @param string $phone   User phone.
	 * @param string $source  Consent source.
	 * @return array Result with success/error.
	 */
	public function save_contact( $email, $name = '', $phone = '', $source = 'chat' ) {
		global $wpdb;

		if ( ! $this->ensure_table_schema() ) {
			return array(
				'success' => false,
				'message' => 'Contact storage is not available.',
			);
		}

		$table_name = $this->get_table_name();
		$event_email = (string) $email;

		// Phone-only mode: generate placeholder email when email is empty but phone is provided.
		if ( empty( $email ) && ! empty( $phone ) ) {
			// Check existing contact by phone number.
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$existing_by_phone = $wpdb->get_var(
				$wpdb->prepare( "SELECT id FROM {$table_name} WHERE phone = %s AND phone != '' LIMIT 1", $phone )
			);

			if ( $existing_by_phone ) {
				$update_data = array();
				if ( ! empty( $name ) ) {
					$update_data['name'] = $name;
				}
				if ( ! empty( $update_data ) ) {
					$update_data['updated_at'] = current_time( 'mysql' );
					$wpdb->update( $table_name, $update_data, array( 'id' => $existing_by_phone ) );
				}
				do_action( 'niwa_ai_contact_saved', '', $name, $phone, $source, (int) $existing_by_phone );
				return array(
					'success' => true,
					'message' => 'Contact updated.',
					'id'      => (int) $existing_by_phone,
				);
			}

			// Generate placeholder email to satisfy UNIQUE KEY constraint.
			$email = 'phone_only_' . md5( $phone ) . '@placeholder.local';
		}

		// Check for existing contact by email.
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$existing = $wpdb->get_var(
			$wpdb->prepare( "SELECT id FROM {$table_name} WHERE email = %s", $email )
		);

		if ( $existing ) {
			// Update existing contact.
			$update_data = array();
			if ( ! empty( $name ) ) {
				$update_data['name'] = $name;
			}
			if ( ! empty( $phone ) ) {
				$update_data['phone'] = $phone;
			}

			if ( ! empty( $update_data ) ) {
				$update_data['updated_at'] = current_time( 'mysql' );
				$wpdb->update( $table_name, $update_data, array( 'id' => $existing ) );
			}

			/**
			 * Fires after a contact row is updated (existing visitor). Used by
			 * the Cart Recovery service to link a shadow cart row to the
			 * now-known email without polluting the save code path with module
			 * concerns. See Niwa_AI_Cart_Recovery_Service::handle_contact_saved.
			 *
			 * @param string $email  Contact email.
			 * @param string $name   Contact name (may be empty).
			 * @param string $phone  Contact phone (may be empty).
			 * @param string $source Consent source tag.
			 * @param int    $id     Row id in niwa_ai_contacts.
			 */
			do_action( 'niwa_ai_contact_saved', $event_email, $name, $phone, $source, (int) $existing );

			return array(
				'success' => true,
				'message' => 'Contact updated.',
				'id'      => (int) $existing,
			);
		}

		// Insert new contact.
		$result = $wpdb->insert(
			$table_name,
			array(
				'email'             => $email,
				'name'              => $name,
				'phone'             => $phone,
				'consent_given'     => 1,
				'consent_source'    => sanitize_text_field( $source ),
				'consent_timestamp' => current_time( 'mysql' ),
			),
			array( '%s', '%s', '%s', '%d', '%s', '%s' )
		);

		if ( false === $result ) {
			return array(
				'success' => false,
				'error'   => 'Database insert failed.',
			);
		}

		$new_id = (int) $wpdb->insert_id;

		// Send immediate notification email if configured.
		$this->maybe_send_notification( $email, $name, $phone );

		/** See niwa_ai_contact_saved docblock above — fired on insert branch. */
		do_action( 'niwa_ai_contact_saved', $event_email, $name, $phone, $source, $new_id );

		return array(
			'success' => true,
			'message' => 'Contact saved successfully.',
			'id'      => $new_id,
		);
	}

	/**
	 * Send notification email to admin and queue for async retry.
	 *
	 * @param string $email User email.
	 * @param string $name  User name.
	 * @param string $phone User phone.
	 */
	private function maybe_send_notification( $email, $name, $phone ) {
		$lead_opts          = class_exists( 'Niwa_AI_Lead_Capture_Settings' ) ? Niwa_AI_Lead_Capture_Settings::get_settings() : array();
		$notification_email = isset( $lead_opts['lead_notification_email'] ) ? $lead_opts['lead_notification_email'] : '';

		if ( empty( $notification_email ) || ! is_email( $notification_email ) ) {
			return;
		}

		$subject = sprintf( '[Niwa AI] New contact: %s', $email );
		$body    = sprintf(
			"New contact captured via Niwa AI chat:\n\nEmail: %s\nName: %s\nPhone: %s\nTime: %s",
			$email,
			$name ?: '(not provided)',
			$phone ?: '(not provided)',
			current_time( 'mysql' )
		);

		$sent = wp_mail( $notification_email, $subject, $body );

		// Queue for async retry if immediate send fails.
		if ( ! $sent ) {
			$this->queue_lead_notification( $email, $name, $phone );
		}
	}

	/**
	 * Queue a lead notification for async retry via cron.
	 *
	 * @param string $email User email.
	 * @param string $name  User name.
	 * @param string $phone User phone.
	 */
	public function queue_lead_notification( $email, $name, $phone ) {
		$queue = Niwa_AI_Durable_Cache::get( 'niwa_ai_lead_notification_queue' );
		if ( ! is_array( $queue ) ) {
			$queue = array();
		}

		$queue[] = array(
			'email'   => $email,
			'name'    => $name,
			'phone'   => $phone,
			'retries' => 0,
			'queued'  => time(),
		);

		Niwa_AI_Durable_Cache::set( 'niwa_ai_lead_notification_queue', $queue, 7 * DAY_IN_SECONDS );
	}

	/**
	 * Process queued lead notifications (called by cron).
	 * Max 10 per run, max 3 retries per notification.
	 */
	public function process_lead_notifications() {
		$queue = Niwa_AI_Durable_Cache::get( 'niwa_ai_lead_notification_queue' );
		if ( ! is_array( $queue ) || empty( $queue ) ) {
			return;
		}

		$remaining = array();
		$processed = 0;

		foreach ( $queue as $notification ) {
			if ( $processed >= 10 ) {
				$remaining[] = $notification;
				continue;
			}

			$retries = isset( $notification['retries'] ) ? (int) $notification['retries'] : 0;

			if ( $retries >= 3 ) {
				// Max retries exceeded — drop it.
				continue;
			}

			$sent = $this->send_queued_notification( $notification );
			$processed++;

			if ( ! $sent ) {
				$notification['retries'] = $retries + 1;
				$remaining[]             = $notification;
			}
		}

		if ( ! empty( $remaining ) ) {
			Niwa_AI_Durable_Cache::set( 'niwa_ai_lead_notification_queue', $remaining, 7 * DAY_IN_SECONDS );
		} else {
			Niwa_AI_Durable_Cache::delete( 'niwa_ai_lead_notification_queue' );
		}
	}

	/**
	 * Send a single queued notification.
	 *
	 * @param array $notification Notification data.
	 * @return bool Success.
	 */
	public function send_queued_notification( $notification ) {
		$lead_opts          = class_exists( 'Niwa_AI_Lead_Capture_Settings' ) ? Niwa_AI_Lead_Capture_Settings::get_settings() : array();
		$notification_email = isset( $lead_opts['lead_notification_email'] ) ? $lead_opts['lead_notification_email'] : '';

		if ( empty( $notification_email ) || ! is_email( $notification_email ) ) {
			return true; // No destination — consider it handled.
		}

		$email = isset( $notification['email'] ) ? $notification['email'] : '';
		$name  = isset( $notification['name'] ) ? $notification['name'] : '';
		$phone = isset( $notification['phone'] ) ? $notification['phone'] : '';

		$subject = sprintf( '[Niwa AI] New contact: %s (retry)', $email );
		$body    = sprintf(
			"New contact captured via Niwa AI chat:\n\nEmail: %s\nName: %s\nPhone: %s\nTime: %s",
			$email,
			$name ?: '(not provided)',
			$phone ?: '(not provided)',
			isset( $notification['queued'] ) ? wp_date( 'Y-m-d H:i:s', (int) $notification['queued'] ) : 'unknown'
		);

		return (bool) wp_mail( $notification_email, $subject, $body );
	}

	/**
	 * Get aggregate stats for admin dashboard.
	 *
	 * @return array { total, last_24h, last_7d, last_30d, with_name, with_consent }
	 */
	public function get_stats() {
		global $wpdb;

		$table = $this->get_table_name();

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$total      = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" );
		$last_24h   = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table} WHERE created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)" );
		$last_7d    = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table} WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)" );
		$last_30d   = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table} WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)" );
		$with_name  = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table} WHERE name != '' AND name IS NOT NULL" );
		$with_consent = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table} WHERE consent_given = 1" );
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		return array(
			'total'          => $total,
			'last_24h'       => $last_24h,
			'last_7d'        => $last_7d,
			'last_30d'       => $last_30d,
			'with_name'      => $with_name,
			'with_consent'   => $with_consent,
			'name_rate'      => $total > 0 ? round( ( $with_name / $total ) * 100, 1 ) : 0,
			'consent_rate'   => $total > 0 ? round( ( $with_consent / $total ) * 100, 1 ) : 0,
		);
	}

	/**
	 * Search contacts with pagination.
	 *
	 * @param string $search  Search term.
	 * @param int    $limit   Per page.
	 * @param int    $offset  Offset.
	 * @return array { items: array, total: int }
	 */
	public function search_contacts( $search = '', $limit = 20, $offset = 0 ) {
		global $wpdb;
		if ( ! $this->ensure_table_schema() ) {
			return array( 'items' => array(), 'total' => 0 );
		}

		$table = $this->get_table_name();

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		if ( '' !== $search ) {
			$like  = '%' . $wpdb->esc_like( $search ) . '%';
			$items = $wpdb->get_results(
				$wpdb->prepare(
					"SELECT * FROM {$table} WHERE email LIKE %s OR name LIKE %s OR phone LIKE %s OR details LIKE %s OR conversation_summary LIKE %s ORDER BY created_at DESC LIMIT %d OFFSET %d",
					$like, $like, $like, $like, $like, $limit, $offset
				),
				ARRAY_A
			);
			$total = (int) $wpdb->get_var(
				$wpdb->prepare(
					"SELECT COUNT(*) FROM {$table} WHERE email LIKE %s OR name LIKE %s OR phone LIKE %s OR details LIKE %s OR conversation_summary LIKE %s",
					$like, $like, $like, $like, $like
				)
			);
		} else {
			$items = $wpdb->get_results(
				$wpdb->prepare(
					"SELECT * FROM {$table} ORDER BY created_at DESC LIMIT %d OFFSET %d",
					$limit, $offset
				),
				ARRAY_A
			);
			$total = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" );
		}
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		return array(
			'items' => $items,
			'total' => $total,
		);
	}

	/**
	 * Fetch one contact row by id.
	 *
	 * @param int $id Contact ID.
	 * @return array|null
	 */
	public function get_contact( $id ) {
		global $wpdb;
		if ( ! $this->ensure_table_schema() ) {
			return null;
		}

		$table = $this->get_table_name();
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$row = $wpdb->get_row(
			$wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d LIMIT 1", (int) $id ),
			ARRAY_A
		);

		return is_array( $row ) ? $row : null;
	}

	/**
	 * Update the editable CRM details for a captured contact.
	 *
	 * @param int    $id      Contact ID.
	 * @param string $details Internal prospect details.
	 * @return bool
	 */
	public function update_contact_details( $id, $details ) {
		global $wpdb;

		$id = (int) $id;
		if ( $id <= 0 ) {
			return false;
		}

		if ( ! $this->ensure_table_schema() ) {
			return false;
		}
		$result = $wpdb->update(
			$this->get_table_name(),
			array(
				'details'    => sanitize_textarea_field( (string) $details ),
				'updated_at' => current_time( 'mysql' ),
			),
			array( 'id' => $id ),
			array( '%s', '%s' ),
			array( '%d' )
		);

		return false !== $result;
	}

	/**
	 * Store an AI-produced conversation summary without overwriting a note
	 * that an administrator has manually changed.
	 *
	 * The editable details field follows the latest summary only while it is
	 * empty or still equal to the previous generated summary. Once a human
	 * edits it, future appointment summaries remain available separately in
	 * conversation_summary and the manual record is preserved.
	 *
	 * @param int    $id      Contact ID.
	 * @param string $summary Generated factual session summary.
	 * @return bool
	 */
	public function apply_conversation_summary( $id, $summary ) {
		global $wpdb;

		$id      = (int) $id;
		$summary = sanitize_textarea_field( (string) $summary );
		if ( $id <= 0 || '' === trim( $summary ) ) {
			return false;
		}

		if ( ! $this->ensure_table_schema() ) {
			return false;
		}
		$contact = $this->get_contact( $id );
		if ( ! $contact ) {
			return false;
		}

		$details         = isset( $contact['details'] ) ? trim( (string) $contact['details'] ) : '';
		$previous_summary = isset( $contact['conversation_summary'] ) ? trim( (string) $contact['conversation_summary'] ) : '';
		$data            = array(
			'conversation_summary' => $summary,
			'updated_at'           => current_time( 'mysql' ),
		);
		$formats         = array( '%s', '%s' );
		if ( '' === $details || ( '' !== $previous_summary && $details === $previous_summary ) ) {
			$data['details'] = $summary;
			$formats[]       = '%s';
		}

		$result = $wpdb->update(
			$this->get_table_name(),
			$data,
			array( 'id' => $id ),
			$formats,
			array( '%d' )
		);

		return false !== $result;
	}

	/**
	 * Delete a contact by ID.
	 *
	 * @param int $id Contact ID.
	 * @return bool Success.
	 */
	public function delete_contact( $id ) {
		global $wpdb;

		$table_name = $this->get_table_name();

		return (bool) $wpdb->delete( $table_name, array( 'id' => (int) $id ), array( '%d' ) );
	}

}
