<?php
/**
 * Dashboard triage: pure partition/grouping logic for the admin dashboard.
 *
 * Splits conversation rows into Follow-ups (unread with captured contact),
 * Activity (everything real), and Brief (no-info rows aggregated into one
 * line per day). WP-independent by contract so it is testable under plain
 * PHP — see tests/dev/dashboard-triage-self-test.php.
 *
 * @link       https://github.com/sitestaffr/sitestaffr-plugin
 * @since      1.34.1
 *
 * @package    SiteStaffr
 * @subpackage SiteStaffr/includes
 */

// If this file is called directly, abort.
if ( ! defined( 'WPINC' ) ) {
	die;
}

/**
 * Pure static helpers that decide where a conversation row belongs on the
 * dashboard. No DB access, no WP functions — rows in, buckets out.
 */
class SiteStaffr_Dashboard_Triage {

	/**
	 * A follow-up is a not-yet-handled conversation the visitor left contact
	 * info in: the receptionist's message slip waiting on the desk. Gated on the
	 * deliberate handled_at flag — NOT is_read — so merely opening a transcript
	 * for context never clears a lead that still needs a reply (matches the
	 * public leads sheet's "Follow-up" vs "Handled" split).
	 *
	 * @param object $call Call row.
	 * @return bool
	 */
	public static function is_follow_up( $call ) {
		if ( self::is_handled( $call ) ) {
			return false;
		}
		return self::has_contact( $call );
	}

	/**
	 * Whether the row was deliberately marked handled/followed-up (handled_at set).
	 *
	 * @param object $call Call row.
	 * @return bool
	 */
	private static function is_handled( $call ) {
		if ( empty( $call->handled_at ) ) {
			return false;
		}
		return '0000-00-00 00:00:00' !== (string) $call->handled_at;
	}

	/**
	 * A brief no-info row: nothing captured and nothing said worth reading.
	 * These aggregate into a single "N brief calls — no message left" line.
	 *
	 * @param object $call Call row.
	 * @return bool
	 */
	public static function is_brief_no_info( $call ) {
		if ( self::has_contact( $call ) ) {
			return false;
		}
		$summary = isset( $call->ai_summary ) ? trim( (string) $call->ai_summary ) : '';
		if ( '' === $summary ) {
			return true;
		}
		return false !== stripos( $summary, 'no information collected' );
	}

	/**
	 * Partition rows into follow_ups / activity / brief buckets (input order kept).
	 *
	 * @param object[] $calls Call rows.
	 * @return array{follow_ups: object[], activity: object[], brief: object[]}
	 */
	public static function partition( $calls ) {
		$parts = array(
			'follow_ups' => array(),
			'activity'   => array(),
			'brief'      => array(),
		);
		foreach ( (array) $calls as $call ) {
			if ( self::is_follow_up( $call ) ) {
				$parts['follow_ups'][] = $call;
			} elseif ( self::is_brief_no_info( $call ) ) {
				$parts['brief'][] = $call;
			} else {
				$parts['activity'][] = $call;
			}
		}
		return $parts;
	}

	/**
	 * Day bucket key for a timestamp: 'today', 'yesterday', or 'Y-m-d'.
	 *
	 * Both timestamps must already be in the display timezone (the caller
	 * converts; this class stays timezone-agnostic).
	 *
	 * @param int $call_ts Row timestamp.
	 * @param int $now_ts  Current timestamp.
	 * @return string
	 */
	public static function day_key( $call_ts, $now_ts ) {
		$call_day = gmdate( 'Y-m-d', (int) $call_ts );
		if ( gmdate( 'Y-m-d', (int) $now_ts ) === $call_day ) {
			return 'today';
		}
		if ( gmdate( 'Y-m-d', (int) $now_ts - self::DAY_SECONDS ) === $call_day ) {
			return 'yesterday';
		}
		return $call_day;
	}

	/**
	 * One day in seconds (WP's DAY_IN_SECONDS without requiring WP to load).
	 *
	 * @var int
	 */
	const DAY_SECONDS = 86400;

	/**
	 * Group non-follow-up rows into ordered day buckets, separating real
	 * activity from brief no-info rows within each day.
	 *
	 * @param object[] $calls  Call rows, newest first.
	 * @param int      $now_ts Current timestamp (display timezone).
	 * @return array<string, array{calls: object[], brief: object[]}>
	 */
	public static function group_activity_by_day( $calls, $now_ts ) {
		$groups = array();
		foreach ( (array) $calls as $call ) {
			$call_ts = isset( $call->created_at ) ? strtotime( (string) $call->created_at ) : 0;
			$key     = self::day_key( $call_ts, $now_ts );
			if ( ! isset( $groups[ $key ] ) ) {
				$groups[ $key ] = array(
					'calls' => array(),
					'brief' => array(),
				);
			}
			if ( self::is_brief_no_info( $call ) ) {
				$groups[ $key ]['brief'][] = $call;
			} else {
				$groups[ $key ]['calls'][] = $call;
			}
		}
		return $groups;
	}

	/**
	 * Whether the row captured any way to reach the visitor.
	 *
	 * @param object $call Call row.
	 * @return bool
	 */
	private static function has_contact( $call ) {
		if ( ! empty( $call->is_qualified_lead ) ) {
			return true;
		}
		$phone = isset( $call->lead_phone ) ? trim( (string) $call->lead_phone ) : '';
		$email = isset( $call->lead_email ) ? trim( (string) $call->lead_email ) : '';
		return '' !== $phone || '' !== $email;
	}
}
