<?php
/**
 * Sites registry: CRUD, key generation/verification, sync bookkeeping.
 */

defined( 'ABSPATH' ) || exit;

class BW_Guides_Server_Sites {

	const TABLE = 'bw_guides_sites';

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

	public static function all() {
		global $wpdb;
		$table = self::table_name();
		return $wpdb->get_results( "SELECT * FROM {$table} ORDER BY site_name ASC" );
	}

	public static function get( $id ) {
		global $wpdb;
		$table = self::table_name();
		return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", $id ) );
	}

	/**
	 * Create a site entry and generate its key.
	 *
	 * @param string $name        Site name.
	 * @param string $url         Site URL.
	 * @param string $client_slug Optional client-group slug (sanitized via
	 *                            sanitize_title so it compares equal to
	 *                            a guide's _bw_guides_target_client verbatim).
	 * @return array|WP_Error array( 'id' => int, 'key' => string ) — the raw key
	 *                        is returned exactly once and never stored.
	 */
	public static function create( $name, $url, $client_slug = '' ) {
		global $wpdb;
		$inserted = $wpdb->insert(
			self::table_name(),
			array(
				'site_name'   => $name,
				'site_url'    => $url,
				'status'      => 'active',
				'created_at'  => current_time( 'mysql', 1 ),
				'client_slug' => self::normalize_client_slug( $client_slug ),
			),
			array( '%s', '%s', '%s', '%s', '%s' )
		);
		if ( ! $inserted || ! $wpdb->insert_id ) {
			return new WP_Error( 'bw_guides_db_error', __( 'Could not create the site entry.', 'bw-guides-server' ) );
		}
		$id  = (int) $wpdb->insert_id;
		$key = self::set_new_key( $id );
		return array(
			'id'  => $id,
			'key' => $key,
		);
	}

	/**
	 * @return string|WP_Error The new raw key (shown once), or error if site missing.
	 */
	public static function regenerate_key( $id ) {
		$row = self::get( $id );
		if ( ! $row ) {
			return new WP_Error( 'bw_guides_not_found', __( 'Site not found.', 'bw-guides-server' ) );
		}
		return self::set_new_key( (int) $row->id );
	}

	private static function set_new_key( $id ) {
		global $wpdb;
		// 20 random bytes = 160 bits of CSPRNG entropy. SHA-256 (not password_hash)
		// is deliberate: the token has enough entropy that brute force is infeasible,
		// and a deterministic hash allows O(1) lookup plus hash_equals() comparison.
		$key = 'bwg_' . $id . '_' . bin2hex( random_bytes( 20 ) );
		$wpdb->update(
			self::table_name(),
			array(
				'key_hash' => hash( 'sha256', $key ),
				'key_hint' => substr( $key, -6 ),
			),
			array( 'id' => $id ),
			array( '%s', '%s' ),
			array( '%d' )
		);
		return $key;
	}

	public static function set_status( $id, $status ) {
		global $wpdb;
		if ( ! in_array( $status, array( 'active', 'revoked' ), true ) ) {
			return false;
		}
		return (bool) $wpdb->update(
			self::table_name(),
			array( 'status' => $status ),
			array( 'id' => (int) $id ),
			array( '%s' ),
			array( '%d' )
		);
	}

	public static function delete( $id ) {
		global $wpdb;
		return (bool) $wpdb->delete( self::table_name(), array( 'id' => (int) $id ), array( '%d' ) );
	}

	/**
	 * Set (or clear) a site's client-group slug.
	 *
	 * @param int    $id          Site id.
	 * @param string $client_slug Raw slug; normalized before storage. Empty
	 *                            string clears client-group targeting for this site.
	 * @return bool
	 */
	public static function set_client_slug( $id, $client_slug ) {
		global $wpdb;
		return (bool) $wpdb->update(
			self::table_name(),
			array( 'client_slug' => self::normalize_client_slug( $client_slug ) ),
			array( 'id' => (int) $id ),
			array( '%s' ),
			array( '%d' )
		);
	}

	/**
	 * Distinct non-empty client slugs currently in use, for admin UI
	 * datalist/autocomplete (Sites screen + guide Targeting metabox).
	 *
	 * @return string[]
	 */
	public static function client_slugs() {
		global $wpdb;
		$table = self::table_name();
		$slugs = $wpdb->get_col( "SELECT DISTINCT client_slug FROM {$table} WHERE client_slug != '' ORDER BY client_slug ASC" );
		return array_values( array_filter( (array) $slugs ) );
	}

	/**
	 * Normalize a client-group slug so site.client_slug and a guide's
	 * _bw_guides_target_client always compare equal with plain ===.
	 *
	 * @param string $raw
	 * @return string
	 */
	public static function normalize_client_slug( $raw ) {
		$slug = sanitize_title( (string) $raw );
		return substr( $slug, 0, 64 );
	}

	/**
	 * Verify a raw site key. Returns the site row on success.
	 *
	 * @return object|WP_Error
	 */
	public static function verify_key( $raw ) {
		if ( ! is_string( $raw ) || ! preg_match( '/^bwg_(\d+)_([0-9a-f]{40})$/', $raw, $m ) ) {
			return new WP_Error( 'bw_guides_invalid_key', __( 'Invalid or missing site key.', 'bw-guides-server' ), array( 'status' => 401 ) );
		}
		$row = self::get( (int) $m[1] );
		if ( ! $row || empty( $row->key_hash ) || ! hash_equals( $row->key_hash, hash( 'sha256', $raw ) ) ) {
			return new WP_Error( 'bw_guides_invalid_key', __( 'Invalid or missing site key.', 'bw-guides-server' ), array( 'status' => 401 ) );
		}
		if ( 'active' !== $row->status ) {
			return new WP_Error( 'bw_guides_key_revoked', __( 'This site key has been revoked.', 'bw-guides-server' ), array( 'status' => 403 ) );
		}
		return $row;
	}

	public static function touch( $id, $ip, $client_version ) {
		global $wpdb;
		$wpdb->update(
			self::table_name(),
			array(
				'last_sync_at'        => current_time( 'mysql', 1 ),
				'last_sync_ip'        => substr( (string) $ip, 0, 64 ),
				'last_client_version' => substr( (string) $client_version, 0, 32 ),
			),
			array( 'id' => (int) $id ),
			array( '%s', '%s', '%s' ),
			array( '%d' )
		);
	}
}
