<?php

namespace SugarCalendarBookings\Appointments;

use DateTimeImmutable;
use SugarCalendarBookings\Customers\CustomerModel;
use SugarCalendarBookings\Employees\EmployeeModel;
use SugarCalendarBookings\Locations\AbstractLocation;
use SugarCalendarBookings\Locations\LocationFactory;
use SugarCalendarBookings\Locations\LocationModel;
use SugarCalendarBookings\Payments\PaymentModel;
use SugarCalendarBookings\Payments\PaymentsHelpers;
use SugarCalendarBookings\Services\ServiceModel;
use SugarCalendarBookings\Support\Activities\ActivityModel;
use SugarCalendarBookings\Support\Database\AbstractModel;
use SugarCalendarBookings\Support\Database\Relations\BelongsToManyRelation;
use SugarCalendarBookings\Support\Database\Relations\BelongsToRelation;
use SugarCalendarBookings\Support\Database\Relations\MorphManyRelation;

/**
 * Appointment model.
 *
 * @since 1.0.0
 *
 * @property int                  $id
 * @property string               $code
 * @property int                  $service_id
 * @property int                  $employee_id
 * @property int                  $customer_id
 * @property ?int                 $location_id
 * @property DateTimeImmutable    $start_at
 * @property DateTimeImmutable    $end_at
 * @property string               $status
 * @property array|null           $online_location
 * @property boolean              $ignore_availability
 * @property string               $customer_timezone
 * @property string               $internal_notes
 * @property string               $customer_notes
 * @property DateTimeImmutable    $created_at
 * @property DateTimeImmutable    $updated_at
 * @property ServiceModel         $service
 * @property EmployeeModel        $employee
 * @property CustomerModel        $customer
 * @property ?LocationModel       $location
 * @property array<PaymentModel>  $payments
 * @property array<ActivityModel> $notes
 * @property array<ActivityModel> $activity_logs
 */
class AppointmentModel extends AbstractModel {

	/**
	 * Draft status.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	const STATUS_DRAFT = 'draft';

	/**
	 * Pending status.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	const STATUS_PENDING = 'pending';

	/**
	 * Approved status.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	const STATUS_APPROVED = 'approved';

	/**
	 * Cancelled status.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	const STATUS_CANCELLED = 'cancelled';

	/**
	 * Done status.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	const STATUS_DONE = 'done';

	/**
	 * The database table name without prefix.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	protected static $table_name = 'appointments';

	/**
	 * The model type identifier.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	protected static $type = 'appointment';

	/**
	 * Column formats for the model.
	 *
	 * @since 1.0.0
	 *
	 * @var array<string, string>
	 */
	protected static $column_formats = [
		'id'          => '%d',
		'code'        => '%s',
		'service_id'  => '%d',
		'employee_id' => '%d',
		'customer_id' => '%d',
		'location_id' => '%d',
		'start_at'    => '%s',
		'end_at'      => '%s',
		'status'      => '%s',
		'created_at'  => '%s',
		'updated_at'  => '%s',
	];

	/**
	 * Meta keys for the model.
	 *
	 * @since 1.0.0
	 *
	 * @var array<string, mixed>
	 */
	protected static $meta_keys = [
		'online_location',
		'provider_meeting_id',
		'ignore_availability',
		'customer_timezone',
		'reminder_sent',
		'internal_notes',
		'customer_notes',
		'_amelia_id',
	];

	/**
	 * The attributes and meta that should be cast.
	 *
	 * @since 1.0.0
	 *
	 * @var array<string, string>
	 */
	protected static $casts = [
		'id'                  => 'integer',
		'service_id'          => 'integer',
		'employee_id'         => 'integer',
		'customer_id'         => 'integer',
		'location_id'         => 'integer',
		'start_at'            => 'datetime',
		'end_at'              => 'datetime',
		'online_location'     => 'array',
		'ignore_availability' => 'boolean',
		'created_at'          => 'datetime',
		'updated_at'          => 'datetime',
		'reminder_sent'       => 'boolean',
	];

	/**
	 * Column defaults for the model.
	 *
	 * @since 1.0.0
	 *
	 * @return array<string, mixed>
	 */
	protected static function column_defaults() {

		return [
			'status'         => self::STATUS_PENDING,
			'internal_notes' => '',
			'customer_notes' => '',
		];
	}

	/**
	 * Save the appointment to the database.
	 *
	 * @since 1.0.0
	 *
	 * @return bool True on success, false on failure.
	 */
	public function save() {

		if ( ! $this->code ) {
			$this->code = $this->generate_code();
		}

		return parent::save();
	}

	/**
	 * Get the employee relationship for the appointment.
	 *
	 * @since 1.0.0
	 *
	 * @return BelongsToRelation
	 */
	public function employee() {

		return $this->belongs_to( EmployeeModel::class );
	}

	/**
	 * Get the services relationship for the appointment.
	 *
	 * @since 1.0.0
	 *
	 * @return BelongsToRelation
	 */
	public function service() {

		return $this->belongs_to( ServiceModel::class );
	}

	/**
	 * Get the customers relationship for the appointment.
	 *
	 * @since 1.0.0
	 *
	 * @return BelongsToRelation
	 */
	public function customer() {

		return $this->belongs_to( CustomerModel::class );
	}

	/**
	 * Get the payments relationship for the appointment.
	 *
	 * @since 1.0.0
	 *
	 * @return BelongsToManyRelation
	 */
	public function payments() {

		return $this->belongs_to_many( PaymentModel::class, 'payment_items', 'item_id', 'payment_id' );
	}

	/**
	 * Get the location relationship for the appointment.
	 *
	 * @since 1.0.0
	 *
	 * @return BelongsToRelation
	 */
	public function location() {

		return $this->belongs_to( LocationModel::class );
	}

	/**
	 * Get the notes relationship for the appointment.
	 *
	 * @since 1.0.0
	 *
	 * @return MorphManyRelation
	 */
	public function notes() {

		return $this->morph_many( ActivityModel::class )
			->where( 'type', '=', 'note', '%s' );
	}

	/**
	 * Get the activity logs relationship for the appointment.
	 *
	 * @since 1.0.0
	 *
	 * @return MorphManyRelation
	 */
	public function activity_logs() {

		return $this->morph_many( ActivityModel::class )
			->where( 'type', '=', 'log', '%s' );
	}

	/**
	 * Get the status label.
	 *
	 * @since 1.0.0
	 *
	 * @return string
	 */
	public function get_status_label() {

		$label = '';

		switch ( $this->status ) {
			case self::STATUS_APPROVED:
				$label = esc_html__( 'Approved', 'sugar-calendar-bookings-scheduling-appointments-lite' );
				break;

			case self::STATUS_PENDING:
				$label = esc_html__( 'Pending', 'sugar-calendar-bookings-scheduling-appointments-lite' );
				break;

			case self::STATUS_CANCELLED:
				$label = esc_html__( 'Cancelled', 'sugar-calendar-bookings-scheduling-appointments-lite' );
				break;

			case self::STATUS_DONE:
				$label = esc_html__( 'Done', 'sugar-calendar-bookings-scheduling-appointments-lite' );
				break;

			case self::STATUS_DRAFT:
				$label = esc_html__( 'Draft', 'sugar-calendar-bookings-scheduling-appointments-lite' );
				break;
		}

		return $label;
	}

	/**
	 * Get the duration of the appointment in minutes.
	 *
	 * @since 1.0.0
	 *
	 * @return int
	 */
	public function get_duration() {

		return ( $this->end_at->getTimestamp() - $this->start_at->getTimestamp() ) / 60;
	}

	/**
	 * Get the location of the appointment.
	 *
	 * @since 1.0.0
	 *
	 * @return AbstractLocation|null
	 */
	public function get_location() {

		return LocationFactory::from_appointment( $this );
	}

	/**
	 * Get the price of the appointment.
	 *
	 * @since 1.0.0
	 *
	 * @return float
	 */
	public function get_price() {

		// TODO: Get price from payment item once will have PaymentItemModel.
		$payment = PaymentsHelpers::get_payment( $this );

		return $payment ? $payment->total_amount : 0;
	}

	/**
	 * Generate a unique code for the appointment.
	 *
	 * @since 1.0.0
	 *
	 * @return string
	 */
	private function generate_code() {

		$tries_limit = 10;

		while ( $tries_limit > 0 ) {
			$code = $this->generate_random_code();

			if ( empty( $this->query()->where( 'code', '=', $code, '%s' )->limit( 1 )->get() ) ) {
				return $code;
			}

			--$tries_limit;
		}

		// If we've tried too many times, just return a random code with a higher uniqueness.
		return strtoupper( uniqid() );
	}

	/**
	 * Generate a random code.
	 *
	 * @since 1.0.0
	 *
	 * @return string
	 */
	private function generate_random_code() {

		$length    = 8;
		$alphabet  = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
		$code      = '';
		$max_index = strlen( $alphabet ) - 1;

		for ( $i = 0; $i < $length; $i++ ) {
			$code .= $alphabet[ random_int( 0, $max_index ) ];
		}

		return $code;
	}
}
