<?php
/**
 * Public Transcript Viewer Route Handler
 *
 * Intercepts magic link requests, verifies HMAC signatures,
 * and renders public transcript pages without requiring login.
 *
 * @link       https://github.com/sitestaffr/sitestaffr-plugin
 * @since      1.2.0
 *
 * @package    SiteStaffr
 * @subpackage SiteStaffr/includes
 */

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

/**
 * Transcript Viewer class for rendering public transcript pages.
 *
 * Hooks into template_redirect to intercept magic link URLs,
 * verify their HMAC signatures, load call data, and render
 * a standalone transcript template.
 *
 * @since      1.2.0
 * @package    SiteStaffr
 * @subpackage SiteStaffr/includes
 * @author     SiteStaffr
 */
class SiteStaffr_Transcript_Viewer {

	/**
	 * Intercept magic link requests and render the transcript page.
	 *
	 * Checks for sitestaffr_transcript and sig query parameters,
	 * verifies the HMAC signature, loads call and transcript data,
	 * and includes the public transcript template.
	 *
	 * @since 1.2.0
	 * @return void
	 */
	public function maybe_render_transcript() {
		// Public transcript route — unauthenticated visitors access via HMAC-signed
		// magic link (verified on line 65). WordPress nonces require a logged-in user,
		// so HMAC signature serves the equivalent anti-tampering role here.
		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		if ( ! isset( $_GET['sitestaffr_transcript'] ) || ! isset( $_GET['sig'] ) ) {
			return;
		}

		// Sanitize inputs.
		$call_id   = absint( wp_unslash( $_GET['sitestaffr_transcript'] ) );
		$signature = sanitize_text_field( wp_unslash( $_GET['sig'] ) );
		$expiry    = isset( $_GET['exp'] ) ? absint( wp_unslash( $_GET['exp'] ) ) : 0;
		// phpcs:enable WordPress.Security.NonceVerification.Recommended

		// Private signed surface: never page-cache and never index — same
		// contract as the leads viewer (applies to error pages too).
		if ( ! defined( 'DONOTCACHEPAGE' ) ) {
			define( 'DONOTCACHEPAGE', true );
		}
		do_action( 'litespeed_control_set_nocache', 'sitestaffr transcript viewer' );
		nocache_headers();
		header( 'X-Robots-Tag: noindex, nofollow', true );

		if ( ! $call_id || empty( $signature ) ) {
			status_header( 403 );
			wp_die(
				esc_html__( 'This transcript link is invalid or has expired.', 'sitestaffr' ),
				esc_html__( 'Invalid Link', 'sitestaffr' ),
				array( 'response' => 403 )
			);
		}

		// Verify HMAC signature. Links with an exp param are the scoped, expiring
		// shape (report-issue since 1.31.0, owner notifications since 1.40.0);
		// links without are legacy non-expiring owner links, honoured only until
		// SiteStaffr_Magic_Link::LEGACY_LINK_CUTOFF.
		if ( $expiry > 0 ) {
			if ( $expiry < time() ) {
				status_header( 410 );
				wp_die(
					esc_html__( 'This conversation link has expired. Links to conversations expire so a transcript is not left readable forever. Open the conversation from your SiteStaffr dashboard, or use the link in a more recent notification email.', 'sitestaffr' ),
					esc_html__( 'Link Expired', 'sitestaffr' ),
					array( 'response' => 410 )
				);
			}

			if ( ! SiteStaffr_Magic_Link::verify_scoped( 'transcript-view', $call_id, $expiry, $signature ) ) {
				status_header( 403 );
				wp_die(
					esc_html__( 'This transcript link is invalid or has expired.', 'sitestaffr' ),
					esc_html__( 'Invalid Link', 'sitestaffr' ),
					array( 'response' => 403 )
				);
			}
		} else {
			// Legacy shape: a bare signed id with no expiry. Refuse it after the
			// sunset, with its own message — a generic "invalid link" would read
			// as a bug to an owner clicking a link that used to work.
			if ( SiteStaffr_Magic_Link::legacy_links_expired() ) {
				status_header( 410 );
				wp_die(
					esc_html__( 'This link is from an older email and no longer opens. Conversation links used to last forever; they now expire so a transcript is not left readable indefinitely. Open the conversation from your SiteStaffr dashboard, or use the link in a recent notification email.', 'sitestaffr' ),
					esc_html__( 'Link Expired', 'sitestaffr' ),
					array( 'response' => 410 )
				);
			}

			if ( ! SiteStaffr_Magic_Link::verify( $call_id, $signature ) ) {
				status_header( 403 );
				wp_die(
					esc_html__( 'This transcript link is invalid or has expired.', 'sitestaffr' ),
					esc_html__( 'Invalid Link', 'sitestaffr' ),
					array( 'response' => 403 )
				);
			}
		}

		// Load call record.
		$call = SiteStaffr_Call_Manager::get_call_by_id( $call_id );

		if ( ! $call || ! empty( $call->deleted_at ) ) {
			status_header( 404 );
			wp_die(
				esc_html__( 'This conversation is no longer available.', 'sitestaffr' ),
				esc_html__( 'Not Found', 'sitestaffr' ),
				array( 'response' => 404 )
			);
		}

		// Load transcripts (prefixed variable name matches template).
		$sitestaffr_transcripts = SiteStaffr_Database::get_call_transcripts( $call_id );

		// Format date in Eastern Time.
		$formatted_date = $this->format_call_date( $call );

		// Format duration as M:SS (empty string if zero/null).
		$formatted_duration = ( ! empty( $call->call_duration ) && $call->call_duration > 0 )
			? $this->format_call_duration( $call->call_duration )
			: '';

		// Get business name.
		$settings      = get_option( 'sitestaffr_settings', array() );
		$business_name = ! empty( $settings['business_name'] ) ? stripslashes( $settings['business_name'] ) : '';

		// Logo URL.
		$logo_url = SITESTAFFR_PLUGIN_URL . 'assets/images/logo.png';

		// Parse summary data.
		$summary_data = $this->parse_summary_data( $call->ai_summary );

		// AI followup from dedicated column.
		$ai_followup = ! empty( $call->ai_followup ) ? $call->ai_followup : '';

		// Enqueue transcript viewer stylesheet so wp_head() outputs it.
		wp_enqueue_style(
			'sitestaffr-transcript-viewer',
			SITESTAFFR_PLUGIN_URL . 'assets/css/sitestaffr-transcript-viewer.css',
			array(),
			SITESTAFFR_VERSION
		);

		// Render the template and exit.
		include SITESTAFFR_PLUGIN_DIR . 'templates/public/transcript-viewer.php';
		exit;
	}

	/**
	 * Format a call's date in Eastern Time.
	 *
	 * Handles both Unix timestamps (started_at BIGINT) and MySQL
	 * DATETIME strings (created_at). Millisecond timestamps are
	 * automatically converted to seconds.
	 *
	 * @since  1.2.0
	 * @param  object $call The call record object.
	 * @return string       Formatted date string, e.g. "March 6, 2026 2:30 PM EST".
	 */
	private function format_call_date( $call ) {
		$timestamp = 0;

		if ( ! empty( $call->started_at ) && $call->started_at > 0 ) {
			$timestamp = (int) $call->started_at;

			// Convert milliseconds to seconds if needed.
			if ( $timestamp > 9999999999 ) {
				$timestamp = (int) floor( $timestamp / 1000 );
			}
		}

		try {
			if ( $timestamp > 0 ) {
				$date = new DateTime( '@' . $timestamp );
			} elseif ( ! empty( $call->created_at ) ) {
				// created_at is MySQL DATETIME stored as UTC.
				$date = new DateTime( $call->created_at, new DateTimeZone( 'UTC' ) );
			} else {
				return '';
			}

			$date->setTimezone( new DateTimeZone( 'America/New_York' ) );
			return $date->format( 'F j, Y g:i A T' );
		} catch ( Exception $e ) {
			return '';
		}
	}

	/**
	 * Format call duration as M:SS.
	 *
	 * @since  1.2.0
	 * @param  int    $duration Duration in seconds.
	 * @return string           Formatted string like "2:05".
	 */
	private function format_call_duration( $duration ) {
		$duration = absint( $duration );
		$minutes  = floor( $duration / 60 );
		$seconds  = $duration % 60;

		return sprintf( '%d:%02d', $minutes, $seconds );
	}

	/**
	 * Parse structured data from the AI summary HTML.
	 *
	 * Extracts visitor name, phone, email, reason, follow-up action,
	 * and summary text from the AI-generated summary markup.
	 *
	 * @since  1.2.0
	 * @param  string $summary_html The raw AI summary HTML.
	 * @return array {
	 *     Parsed summary fields.
	 *
	 *     @type string $visitor_name Visitor's name.
	 *     @type string $phone        Phone number.
	 *     @type string $email        Email address.
	 *     @type string $reason       Reason for the call.
	 *     @type string $followup     Suggested follow-up action.
	 *     @type string $summary_text Summary paragraph text.
	 * }
	 */
	private function parse_summary_data( $summary_html ) {
		$data = array(
			'visitor_name' => '',
			'phone'        => '',
			'email'        => '',
			'reason'       => '',
			'followup'     => '',
			'summary_text' => '',
		);

		if ( empty( $summary_html ) ) {
			return $data;
		}

		$summary_html = stripslashes( $summary_html );

		// Extract visitor name.
		if ( preg_match( '/Name:\s*([^\n<]+)/i', $summary_html, $matches ) ) {
			$data['visitor_name'] = trim( $matches[1] );
		} elseif ( preg_match( '/<strong>([^<]+)<\/strong>/i', $summary_html, $matches ) ) {
			$data['visitor_name'] = trim( $matches[1] );
		}

		// Extract phone.
		if ( preg_match( '/Phone:\s*([^\n<]+)/i', $summary_html, $matches ) ) {
			$data['phone'] = trim( $matches[1] );
		}

		// Extract email.
		if ( preg_match( '/Email:\s*([^\n<]+)/i', $summary_html, $matches ) ) {
			$data['email'] = trim( $matches[1] );
		}

		// Extract reason.
		if ( preg_match( '/Reason(?:\s+for\s+call)?:\s*([^\n<]+)/i', $summary_html, $matches ) ) {
			$data['reason'] = trim( $matches[1] );
		}

		// Extract follow-up.
		if ( preg_match( '/Suggested\s+follow[- ]?up:\s*([^\n<]+)/i', $summary_html, $matches ) ) {
			$data['followup'] = trim( $matches[1] );
		}

		// Extract summary text: first line of plain text that is not a label.
		$stripped = wp_strip_all_tags( $summary_html );
		$lines    = preg_split( '/\r?\n/', $stripped );

		foreach ( $lines as $line ) {
			$line = trim( $line );
			if ( empty( $line ) ) {
				continue;
			}
			// Skip lines that are labels (Name:, Phone:, Email:, Reason:, Suggested follow-up:).
			if ( preg_match( '/^(Name|Phone|Email|Reason(?:\s+for\s+call)?|Suggested\s+follow[- ]?up):\s*/i', $line ) ) {
				continue;
			}
			$data['summary_text'] = $line;
			break;
		}

		return $data;
	}
}
