<?php
defined( 'ABSPATH' ) || exit;

/**
 * Leads — the *person* half of the data model.
 *
 * The distinction this class exists to enforce: **an anchor is a device, a lead is a
 * person.** One person may browse from several devices; one device may be used by
 * several people. A single row keyed by a browser cookie can represent neither.
 *
 * Cross-device continuity falls out of that for free — two anchors pointing at one
 * `lead_id` is many-to-one. What it costs is that identity resolution has to be
 * explicit, which is what the rules below are.
 *
 * The rule that matters most: **link, never merge; never rewrite past ownership.**
 * If one device produces two identities, we do not guess which one the earlier
 * browsing belonged to — we flag it and stop attributing. On a site whose audience
 * includes minors, silently attributing one person's browsing to another person's
 * record is destructive on write and unrecoverable afterwards.
 */
class BW_Lead_AI_Leads {

	const TABLE_SUFFIX = 'bw_lead_ai_leads';

	/** Where the HMAC pepper lives if the site has not defined a constant. */
	const PEPPER_OPTION = 'bw_lead_ai_email_pepper';

	public static function table_name() {
		global $wpdb;
		return $wpdb->prefix . self::TABLE_SUFFIX;
	}

	/**
	 * The pepper used to derive `email_hmac`.
	 *
	 * A site-wide pepper, not a per-row salt: the hash has to be *looked up* by, and a
	 * per-row salt would make that impossible. Prefer a constant in wp-config.php so it
	 * lives outside the database; fall back to a generated option so the feature works
	 * without manual setup.
	 */
	private static function pepper() {
		if ( defined( 'BW_LEAD_AI_EMAIL_PEPPER' ) && BW_LEAD_AI_EMAIL_PEPPER ) {
			return (string) BW_LEAD_AI_EMAIL_PEPPER;
		}
		$pepper = get_option( self::PEPPER_OPTION );
		if ( ! $pepper ) {
			$pepper = bin2hex( random_bytes( 32 ) );
			// autoload = no: this is a secret, not something every page needs.
			add_option( self::PEPPER_OPTION, $pepper, '', 'no' );
		}
		return (string) $pepper;
	}

	/**
	 * Derive the lookup key for an email address.
	 *
	 * HMAC rather than a bare hash: a plain `sha256(email)` is effectively the email
	 * itself, because the input space is small enough to enumerate. The pepper is what
	 * makes a leaked table of hashes useless without also leaking the pepper.
	 *
	 * Normalised first so `John.Smith@Example.com ` and `john.smith@example.com` match.
	 */
	public static function email_hmac( $email ) {
		$email = strtolower( trim( (string) $email ) );
		if ( '' === $email || ! is_email( $email ) ) {
			return '';
		}
		return hash_hmac( 'sha256', $email, self::pepper() );
	}

	/**
	 * Find an existing lead by email, or create one.
	 *
	 * Exact match only — deliberately. Fuzzy matching on names, phone numbers or
	 * similar-looking addresses is how one family's record silently absorbs another's.
	 *
	 * @param array $identity first, last, email, phone.
	 * @return int|0 Lead id, or 0 when there is nothing identifying to key on.
	 */
	public static function resolve( $identity ) {
		if ( empty( $identity['email'] ) ) {
			// Without an email there is no stable key, so no lead. The journey still
			// gets stored against its anchor; it just is not attributed to a person.
			return 0;
		}
		$hmac = self::email_hmac( $identity['email'] );
		if ( '' === $hmac ) {
			return 0;
		}

		global $wpdb;
		$table = self::table_name();

		$existing = $wpdb->get_var(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from $wpdb->prefix.
				"SELECT id FROM {$table} WHERE email_hmac = %s",
				$hmac
			)
		);

		if ( $existing ) {
			self::enrich( (int) $existing, $identity );
			return (int) $existing;
		}

		$now = current_time( 'mysql', true );
		$wpdb->insert(
			$table,
			array(
				'email_hmac'  => $hmac,
				'first_name'  => isset( $identity['first'] ) ? $identity['first'] : null,
				'last_name'   => isset( $identity['last'] ) ? $identity['last'] : null,
				'email'       => $identity['email'],
				'phone'       => isset( $identity['phone'] ) ? $identity['phone'] : null,
				'lead_status' => '',
				'created_at'  => $now,
				'updated_at'  => $now,
			),
			array( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' )
		);
		return (int) $wpdb->insert_id;
	}

	/**
	 * Fill in details we did not have before. Never overwrites a value with an empty
	 * one — a later form that omits the phone number should not erase it.
	 */
	private static function enrich( $lead_id, $identity ) {
		$map = array( 'first' => 'first_name', 'last' => 'last_name', 'phone' => 'phone' );
		$set = array();
		foreach ( $map as $from => $column ) {
			if ( ! empty( $identity[ $from ] ) ) {
				$set[ $column ] = $identity[ $from ];
			}
		}
		if ( empty( $set ) ) {
			return;
		}
		$set['updated_at'] = current_time( 'mysql', true );

		global $wpdb;
		$wpdb->update( self::table_name(), $set, array( 'id' => (int) $lead_id ) );
	}

	public static function get( $lead_id ) {
		if ( ! $lead_id ) {
			return null;
		}
		global $wpdb;
		return $wpdb->get_row(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from $wpdb->prefix.
				'SELECT * FROM ' . self::table_name() . ' WHERE id = %d',
				(int) $lead_id
			),
			ARRAY_A
		);
	}

	public static function find_by_email( $email ) {
		$hmac = self::email_hmac( $email );
		if ( '' === $hmac ) {
			return null;
		}
		global $wpdb;
		return $wpdb->get_row(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from $wpdb->prefix.
				'SELECT * FROM ' . self::table_name() . ' WHERE email_hmac = %s',
				$hmac
			),
			ARRAY_A
		);
	}

	public static function display_name( $lead ) {
		if ( empty( $lead ) ) {
			return '';
		}
		$name = trim( ( isset( $lead['first_name'] ) ? $lead['first_name'] : '' ) . ' ' . ( isset( $lead['last_name'] ) ? $lead['last_name'] : '' ) );
		if ( '' !== $name ) {
			return $name;
		}
		return isset( $lead['email'] ) ? (string) $lead['email'] : '';
	}

	/**
	 * Attach a device (anchor) to a person (lead), applying the shared-device rule.
	 *
	 * Returns one of:
	 *   'linked'  — the anchor now belongs to this lead
	 *   'same'    — it already did; nothing changed
	 *   'shared'  — this device has now produced two different people. The anchor is
	 *               flagged, its lead link is LEFT AS IT WAS, and its future browsing
	 *               stops being attributed to anyone. We do not guess.
	 *
	 * @param array $anchor  Row from the anchors table.
	 * @param int   $lead_id The lead the current submission resolved to.
	 */
	public static function link_anchor( $anchor, $lead_id ) {
		if ( ! $lead_id || empty( $anchor ) ) {
			return 'none';
		}

		global $wpdb;
		$table   = BW_Lead_AI_Handoff_Store::table_name();
		$current = isset( $anchor['lead_id'] ) ? (int) $anchor['lead_id'] : 0;

		if ( $current === (int) $lead_id ) {
			return 'same';
		}

		if ( $current > 0 ) {
			// One device, two people. Flag it and stop attributing; never reassign the
			// earlier browsing, and never merge the two leads.
			$wpdb->update(
				$table,
				array( 'shared_flag' => 1 ),
				array( 'id' => (int) $anchor['id'] ),
				array( '%d' ),
				array( '%d' )
			);
			return 'shared';
		}

		$wpdb->update(
			$table,
			array( 'lead_id' => (int) $lead_id, 'link_provenance' => 'email_hmac' ),
			array( 'id' => (int) $anchor['id'] ),
			array( '%d', '%s' ),
			array( '%d' )
		);
		return 'linked';
	}

	/**
	 * May this anchor's ongoing browsing be attributed to its lead?
	 *
	 * False once a device is known to be shared — from that point the journey is still
	 * recorded, it is just no longer claimed on anyone's behalf.
	 */
	public static function may_attribute( $anchor ) {
		return ! empty( $anchor['lead_id'] ) && empty( $anchor['shared_flag'] );
	}

	public static function delete_by_email( $email ) {
		$lead = self::find_by_email( $email );
		if ( ! $lead ) {
			return 0;
		}
		global $wpdb;
		// Orphan the devices rather than deleting their journeys outright: the erasure
		// request is about the person, and an unattributed journey identifies nobody.
		$wpdb->update(
			BW_Lead_AI_Handoff_Store::table_name(),
			array( 'lead_id' => null ),
			array( 'lead_id' => (int) $lead['id'] ),
			array( '%d' ),
			array( '%d' )
		);
		return (int) $wpdb->delete( self::table_name(), array( 'id' => (int) $lead['id'] ), array( '%d' ) );
	}
}
