<?php
/**
 * Lead workflow repository.
 *
 * @package SubmissionInbox
 */

namespace SUBIN\Leads;

use SUBIN\Security\Capabilities;

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

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Repository methods update Submission Inbox workflow fields directly.

/**
 * Manages pipeline fields on saved leads.
 */
class LeadWorkflowRepository {
	const WORKFLOW_OPEN      = 'open';
	const WORKFLOW_CONTACTED = 'contacted';
	const WORKFLOW_QUALIFIED = 'qualified';
	const WORKFLOW_WON       = 'won';
	const WORKFLOW_LOST      = 'lost';

	const PRIORITY_LOW    = 'low';
	const PRIORITY_NORMAL = 'normal';
	const PRIORITY_HIGH   = 'high';
	const PRIORITY_URGENT = 'urgent';

	/**
	 * Activity repository.
	 *
	 * @var LeadActivityRepository
	 */
	private $activity;

	/**
	 * Constructor.
	 *
	 * @param LeadActivityRepository|null $activity Activity repository.
	 */
	public function __construct( ?LeadActivityRepository $activity = null ) {
		$this->activity = $activity ? $activity : new LeadActivityRepository();
	}

	/**
	 * Workflow statuses.
	 *
	 * @return array<string,string>
	 */
	public function workflow_statuses() {
		return array(
			self::WORKFLOW_OPEN      => __( 'Open', 'submission-inbox' ),
			self::WORKFLOW_CONTACTED => __( 'Contacted', 'submission-inbox' ),
			self::WORKFLOW_QUALIFIED => __( 'Qualified', 'submission-inbox' ),
			self::WORKFLOW_WON       => __( 'Won', 'submission-inbox' ),
			self::WORKFLOW_LOST      => __( 'Lost', 'submission-inbox' ),
		);
	}

	/**
	 * Priorities.
	 *
	 * @return array<string,string>
	 */
	public function priorities() {
		return array(
			self::PRIORITY_LOW    => __( 'Low', 'submission-inbox' ),
			self::PRIORITY_NORMAL => __( 'Normal', 'submission-inbox' ),
			self::PRIORITY_HIGH   => __( 'High', 'submission-inbox' ),
			self::PRIORITY_URGENT => __( 'Urgent', 'submission-inbox' ),
		);
	}

	/**
	 * Update workflow fields.
	 *
	 * @param int                 $lead_id Lead id.
	 * @param array<string,mixed> $data Workflow data.
	 * @param int                 $actor_user_id Actor user id.
	 * @return bool
	 */
	public function update_workflow( $lead_id, array $data, $actor_user_id = 0 ) {
		$result = $this->persist_workflow( $lead_id, $data, null, $actor_user_id );

		return true === $result;
	}

	/**
	 * Update workflow fields only when the editor still has the current version.
	 *
	 * @param int                 $lead_id Lead id.
	 * @param array<string,mixed> $data Workflow data.
	 * @param mixed               $expected_version Version rendered with the editor.
	 * @param int                 $actor_user_id Actor user id.
	 * @return true|\WP_Error
	 */
	public function update_workflow_if_current( $lead_id, array $data, $expected_version, $actor_user_id = 0 ) {
		if ( ! is_scalar( $expected_version ) || 1 !== preg_match( '/^[1-9]\d*$/', (string) $expected_version ) ) {
			return new \WP_Error(
				'subin_workflow_version_invalid',
				__( 'The workflow editor version is missing or invalid. Refresh the lead and try again.', 'submission-inbox' )
			);
		}

		return $this->persist_workflow( $lead_id, $data, absint( $expected_version ), $actor_user_id );
	}

	/**
	 * Validate and atomically persist a workflow mutation.
	 *
	 * Legacy and bulk callers omit an expected version, but still use the version
	 * read with the row as an atomic compare-and-swap guard. Interactive editors
	 * supply the version they rendered so a stale tab cannot overwrite newer work.
	 *
	 * @param int                 $lead_id Lead id.
	 * @param array<string,mixed> $data Workflow data.
	 * @param int|null            $expected_version Expected workflow version.
	 * @param int                 $actor_user_id Actor user id.
	 * @return true|\WP_Error
	 */
	private function persist_workflow( $lead_id, array $data, $expected_version, $actor_user_id ) {
		global $wpdb;

		$lead_id = absint( $lead_id );
		if ( $lead_id <= 0 ) {
			return new \WP_Error( 'subin_workflow_lead_invalid', __( 'A valid lead is required.', 'submission-inbox' ) );
		}
		$current = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT workflow_status, priority, assigned_user_id, follow_up_at, workflow_version FROM {$wpdb->prefix}subin_leads WHERE id = %d LIMIT 1",
				$lead_id
			)
		);
		if ( ! $current ) {
			return new \WP_Error( 'subin_workflow_lead_missing', __( 'The lead no longer exists.', 'submission-inbox' ) );
		}

		$current_version = max( 1, absint( $current->workflow_version ?? 1 ) );
		if ( null !== $expected_version && $expected_version !== $current_version ) {
			return $this->conflict_error( $current_version );
		}

		$updates = array();

		if ( array_key_exists( 'workflow_status', $data ) ) {
			if ( ! is_scalar( $data['workflow_status'] ) ) {
				return new \WP_Error( 'subin_workflow_value_invalid', __( 'The workflow stage is invalid.', 'submission-inbox' ) );
			}
			$workflow_status = sanitize_key( (string) $data['workflow_status'] );
			if ( ! array_key_exists( $workflow_status, $this->workflow_statuses() ) ) {
				return new \WP_Error( 'subin_workflow_value_invalid', __( 'The workflow stage is invalid.', 'submission-inbox' ) );
			}
			$updates['workflow_status'] = $workflow_status;
		}
		if ( array_key_exists( 'priority', $data ) ) {
			if ( ! is_scalar( $data['priority'] ) ) {
				return new \WP_Error( 'subin_workflow_value_invalid', __( 'The lead priority is invalid.', 'submission-inbox' ) );
			}
			$priority = sanitize_key( (string) $data['priority'] );
			if ( ! array_key_exists( $priority, $this->priorities() ) ) {
				return new \WP_Error( 'subin_workflow_value_invalid', __( 'The lead priority is invalid.', 'submission-inbox' ) );
			}
			$updates['priority'] = $priority;
		}
		if ( array_key_exists( 'assigned_user_id', $data ) ) {
			if ( ! is_scalar( $data['assigned_user_id'] ) ) {
				return new \WP_Error( 'subin_workflow_value_invalid', __( 'The assignee is invalid.', 'submission-inbox' ) );
			}
			if ( 1 !== preg_match( '/^\d+$/', (string) $data['assigned_user_id'] ) ) {
				return new \WP_Error( 'subin_workflow_value_invalid', __( 'The assignee is invalid.', 'submission-inbox' ) );
			}
			$assigned_user_id = absint( $data['assigned_user_id'] );
			$current_assignee  = absint( $current->assigned_user_id ?? 0 );
			if (
				$assigned_user_id > 0
				&& $assigned_user_id !== $current_assignee
				&& (
					! get_userdata( $assigned_user_id )
					|| ! Capabilities::user_can( $assigned_user_id, Capabilities::VIEW_LEADS )
					|| ! Capabilities::user_can( $assigned_user_id, Capabilities::MANAGE_LEADS )
				)
			) {
				return new \WP_Error( 'subin_workflow_assignee_invalid', __( 'The selected user cannot manage leads.', 'submission-inbox' ) );
			}
			$updates['assigned_user_id'] = $assigned_user_id;
		}
		if ( array_key_exists( 'follow_up_at', $data ) ) {
			if ( ! is_scalar( $data['follow_up_at'] ) ) {
				return new \WP_Error( 'subin_workflow_value_invalid', __( 'The follow-up date is invalid.', 'submission-inbox' ) );
			}
			$raw_follow_up = sanitize_text_field( (string) $data['follow_up_at'] );
			$follow_up_at  = $this->sanitize_follow_up_at( $raw_follow_up );
			if ( '' !== $raw_follow_up && null === $follow_up_at ) {
				return new \WP_Error( 'subin_workflow_value_invalid', __( 'The follow-up date is invalid.', 'submission-inbox' ) );
			}
			$updates['follow_up_at'] = $follow_up_at;
		}

		if ( empty( $updates ) ) {
			return new \WP_Error( 'subin_workflow_empty', __( 'No workflow values were supplied.', 'submission-inbox' ) );
		}

		$changes = array();
		foreach ( $updates as $key => $value ) {
			$previous = $current->{$key} ?? null;
			if ( 'assigned_user_id' === $key ) {
				$previous = absint( $previous );
			} elseif ( 'follow_up_at' === $key ) {
				$previous = empty( $previous ) ? null : (string) $previous;
			} else {
				$previous = (string) $previous;
			}

			if ( $previous !== $value ) {
				$changes[ $key ] = array(
					'from' => $previous,
					'to'   => $value,
				);
			}
		}
		if ( empty( $changes ) ) {
			return true;
		}

		$updates      = array_intersect_key( $updates, $changes );
		$set_clauses  = array();
		$query_values = array();
		foreach ( $updates as $key => $value ) {
			if ( 'assigned_user_id' === $key ) {
				$set_clauses[]  = 'assigned_user_id = %d';
				$query_values[] = $value;
			} elseif ( 'follow_up_at' === $key && null === $value ) {
				$set_clauses[] = 'follow_up_at = NULL';
			} elseif ( in_array( $key, array( 'workflow_status', 'priority', 'follow_up_at' ), true ) ) {
				$set_clauses[]  = $key . ' = %s';
				$query_values[] = $value;
			}
		}

		$updated_at     = current_time( 'mysql' );
		$set_clauses[]  = 'updated_at = %s';
		$query_values[] = $updated_at;
		$set_clauses[]  = 'workflow_version = workflow_version + 1';
		$query_values[] = $lead_id;
		$query_values[] = $current_version;
		$query          = "UPDATE {$wpdb->prefix}subin_leads SET " . implode( ', ', $set_clauses ) . ' WHERE id = %d AND workflow_version = %d';
		$result         = $wpdb->query( $wpdb->prepare( $query, $query_values ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Columns are selected from the fixed allowlist above.

		if ( false === $result ) {
			return new \WP_Error( 'subin_workflow_update_failed', __( 'The workflow could not be saved.', 'submission-inbox' ) );
		}
		if ( 0 === $result ) {
			$latest = $wpdb->get_row(
				$wpdb->prepare(
					"SELECT workflow_version FROM {$wpdb->prefix}subin_leads WHERE id = %d LIMIT 1",
					$lead_id
				)
			);
			if ( ! $latest ) {
				return new \WP_Error( 'subin_workflow_lead_missing', __( 'The lead no longer exists.', 'submission-inbox' ) );
			}

			return $this->conflict_error( max( 1, absint( $latest->workflow_version ?? 1 ) ) );
		}

		$updates['updated_at']       = $updated_at;
		$updates['workflow_version'] = $current_version + 1;
		$workflow_snapshot           = array(
			'workflow_status'  => (string) ( $updates['workflow_status'] ?? $current->workflow_status ?? self::WORKFLOW_OPEN ),
			'priority'         => (string) ( $updates['priority'] ?? $current->priority ?? self::PRIORITY_NORMAL ),
			'assigned_user_id' => absint( $updates['assigned_user_id'] ?? $current->assigned_user_id ?? 0 ),
			'follow_up_at'     => array_key_exists( 'follow_up_at', $updates )
				? $updates['follow_up_at']
				: ( empty( $current->follow_up_at ) ? null : (string) $current->follow_up_at ),
			'workflow_version' => $current_version + 1,
		);

		$this->activity->add_activity(
			$lead_id,
			'workflow_updated',
			__( 'Workflow updated.', 'submission-inbox' ),
			array( 'changes' => $changes ),
			$actor_user_id
		);
		do_action( 'subin_lead_workflow_updated', $lead_id, $updates, $changes, $workflow_snapshot );

		return true;
	}

	/**
	 * Build a stable optimistic-concurrency error.
	 *
	 * @param int $current_version Latest workflow version.
	 * @return \WP_Error
	 */
	private function conflict_error( $current_version ) {
		return new \WP_Error(
			'subin_workflow_conflict',
			__( 'This lead changed after the workflow editor was opened.', 'submission-inbox' ),
			array( 'current_version' => max( 1, absint( $current_version ) ) )
		);
	}

	/**
	 * Sanitize workflow status.
	 *
	 * @param mixed $status Status.
	 * @return string
	 */
	public function sanitize_workflow_status( $status ) {
		$status = sanitize_key( (string) $status );

		return array_key_exists( $status, $this->workflow_statuses() ) ? $status : self::WORKFLOW_OPEN;
	}

	/**
	 * Sanitize priority.
	 *
	 * @param mixed $priority Priority.
	 * @return string
	 */
	public function sanitize_priority( $priority ) {
		$priority = sanitize_key( (string) $priority );

		return array_key_exists( $priority, $this->priorities() ) ? $priority : self::PRIORITY_NORMAL;
	}

	/**
	 * Sanitize follow-up date into mysql datetime.
	 *
	 * @param mixed $value Raw value.
	 * @return string|null
	 */
	public function sanitize_follow_up_at( $value ) {
		$value = sanitize_text_field( (string) $value );
		if ( '' === $value ) {
			return null;
		}

		if ( 1 === preg_match( '/^\d{4}-\d{2}-\d{2}$/', $value ) ) {
			$parts = array_map( 'absint', explode( '-', $value ) );
			if ( 3 === count( $parts ) && checkdate( $parts[1], $parts[2], $parts[0] ) ) {
				return $value . ' 09:00:00';
			}
		}

		return null;
	}
}
