<?php

namespace Planaday\Database\SQLDatabaseControllers\Booking;

use Exception;
use Planaday\Database\SQLDatabaseControllers\Database;
use Planaday\Database\SQLDatabaseControllers\Table;
use Planaday\Entities\Booking\BookingDatabase;
use Planaday\Entities\Booking\DTO\Address;
use Planaday\Entities\Booking\DTO\Booking;
use Planaday\Entities\Booking\DTO\BookingForm;
use Planaday\Entities\Booking\DTO\BookingStudent;
use Planaday\Entities\Booking\DTO\Transaction;
use Planaday\Entities\Booking\Enum\BookingStatus;
use Planaday\Utility\DataGrouping\Collection;
use Planaday\Utility\DataGrouping\UUIDGenerator;
use Planaday\Utility\Debug\Log\Logging;

class SQLBookingDatabase implements BookingDatabase, Database {
	use Logging;

	public function __construct(
		protected BookingAddressTable $bookingAddressTable,
		protected BookingAttributeTable $bookingAttributeTable,
		protected BookingCompanyTable $bookingCompanyTable,
		protected BookingMaterialTable $bookingMaterialTable,
		protected BookingStudentTable $bookingStudentTable,
		protected BookingTable $bookingTable,
		protected StudentExtraFieldTable $studentExtraFieldTable,
		protected TransactionTable $transactionTable,
		protected UUIDGenerator $UUIDGenerator,
		protected BookingDayPartLink $bookingDayPartLink
	) {}

	public function getBooking( string $bookingId ): ?Booking {
		$addressCallback = function ( int $addressId ) {
			return $this->bookingAddressTable->get( $addressId );
		};

		$companyCallback = function ( int $companyId ) use ( &$addressCallback ){
			return $this->bookingCompanyTable->get( $companyId, $addressCallback );
		};

		$studentExtraFieldCallback = function ( int $studentId ) {
			return $this->getStudentExtraFields( $studentId );
		};

		$transactionCallback = function ( string $transactionId ) {
			return $this->transactionTable->get( $transactionId );
		};

		$students = $this->bookingStudentTable->getByBookingId($bookingId, $studentExtraFieldCallback, $addressCallback);
		$materials = $this->getBookingMaterials($bookingId);
		$attributes = $this->getBookingAttributes($bookingId);
		$dayParts = $this->bookingDayPartLink->getDayPartsForBooking( $bookingId );

		return $this->bookingTable->get(
			$bookingId,
			$students,
			$attributes,
			$materials,
			$companyCallback,
			$transactionCallback,
			[...$dayParts]
		);
	}

	public function setBooking( Booking $booking ): string {
		$company = $booking->getBookingForm()->getCompany();
		if($company != null){
			$companyAddress = $this->setAddress($company->getAddress());
			if ($company->getInvoice()) $invoiceAddress = $this->setAddress($company->getInvoice()->getAddress());

			$companyId = $this->bookingCompanyTable->set($company, $companyAddress, $invoiceAddress ?? null);
		}

		$transaction = $booking->getTransaction();
		if ($transaction !== null) {
			$this->transactionTable->set($transaction);
			$transactionId = $transaction->getId();
		}

		$bookingId = $this->bookingTable->set($booking, $companyId ?? null, $transactionId ?? null);
		$booking->setDatabaseId($bookingId);

		$this->bookingAttributeTable->removeByBookingId( $bookingId );
		foreach ($booking->getBookingForm()->getAttributes() as $key => $attribute){
			$this->bookingAttributeTable->set( $bookingId, (int) $key, $attribute );
		}

		$this->bookingMaterialTable->removeByBookingId( $bookingId );
		foreach ($booking->getBookingForm()->getMaterials() as $material){
			$this->bookingMaterialTable->set( $bookingId, $material);
		}

		foreach ($booking->getBookingForm()->getStudents() as $student){
			$studentAddress = $this->setAddress($student->getAddress());
			$studentId = $this->bookingStudentTable->set($student, $studentAddress, $bookingId);

			$this->studentExtraFieldTable->removeByStudentId( $studentId );
			foreach ($student->getExtraFields() as $key => $value){
				if ($value === null) continue;
				$this->studentExtraFieldTable->set( $studentId, $key, $value );
			}
		}

		if ($booking->getBookingForm()->getDayParts() !== null) {
			$this->bookingDayPartLink->setDayPartsForBooking( $bookingId, $booking->getBookingForm()->getDayParts() );
		}

		return $bookingId;
	}

	public function removeBooking(string $bookingId): void {
		$booking = $this->getBooking($bookingId);

		foreach ($booking->getBookingForm()->getStudents() as $student){
			$this->studentExtraFieldTable->removeByStudentId( $student->getDatabaseId() );

			$this->bookingStudentTable->remove($student->getDatabaseId());

			if ($student->getAddress()->getDatabaseId() !== null) {
				$this->bookingAddressTable->remove( $student->getAddress()->getDatabaseId() );
			}
		}

		$this->bookingAttributeTable->removeByBookingId($bookingId);
		$this->bookingMaterialTable->removeByBookingId($bookingId);

		$this->bookingTable->remove($bookingId);

		$company = $booking->getBookingForm()->getCompany();
		if ($company !== null) {
			$this->bookingCompanyTable->remove($booking->getBookingForm()->getCompany()->getDatabaseId());

			if ($company->getAddress()->getDatabaseId() !== null) {
				$this->bookingAddressTable->remove( $company->getAddress()->getDatabaseId() );
			}

			if ($company->getInvoice()->getAddress()->getDatabaseId() !== null) {
				$this->bookingAddressTable->remove( $company->getInvoice()->getAddress()->getDatabaseId() );
			}
		}

		if ($booking->getTransaction()) $this->transactionTable->remove($booking->getTransaction()->getId());

		$this->bookingDayPartLink->setDayPartsForBooking( $bookingId, new Collection() );
	}

	protected function setAddress(?Address $address): ?int{
		if (
			$address === null || (
				$address->getName() === null &&
				$address->getCountry() === null &&
				$address->getCity() === null &&
				$address->getStreet() === null &&
				$address->getPostalCode() === null &&
				$address->getHouseNumber() === null &&
				$address->getHouseNumberExtension() === null
			)
		){
			return null;
		}

		return $this->bookingAddressTable->set($address);
	}

	/**
	 * @param string $bookingId
	 *
	 * @return array<string, string|string[]>
	 */
	protected function getBookingAttributes( string $bookingId ): array {
		$attributes = $this->bookingAttributeTable->getByBookingId($bookingId);

		$output = [];

		foreach ($attributes as $attribute) {
			$output[$attribute[BookingAttributeTable::ATTRIBUTE_ID_COLUMN]] = $attribute[BookingAttributeTable::VALUE_COLUMN];
		}

		return $output;
	}

	/**
	 * @param int $studentId
	 *
	 * @return array<int, string|string[]>
	 */
	protected function getStudentExtraFields( int $studentId ): array {
		$extraFields = $this->studentExtraFieldTable->getByStudentId($studentId);

		$output = [];

		foreach ($extraFields as $extraField) {
			$output[$extraField[StudentExtraFieldTable::EXTRA_FIELD_ID_COLUMN]] = $extraField[StudentExtraFieldTable::VALUE_COLUMN];
		}

		return $output;
	}

	/**
	 * @param string $bookingId
	 *
	 * @return array<int>
	 */
	protected function getBookingMaterials( string $bookingId ): array {
		$materials = $this->bookingMaterialTable->getByBookingId($bookingId);

		$output = [];

		foreach ($materials as $material) {
			$output[] = (int)($material[BookingMaterialTable::MATERIAL_ID_COLUMN] ?? 0);
		}

		return $output;
	}

	public function getCreateTableQuery( bool $force = false ): string {
		/** @var Table[] $tables */
		$tables = [
			$this->bookingTable,
			$this->bookingAttributeTable,
			$this->bookingStudentTable,
			$this->bookingMaterialTable,
			$this->bookingCompanyTable,
			$this->bookingAddressTable,
			$this->studentExtraFieldTable,
			$this->transactionTable,
			$this->bookingDayPartLink
		];

		$outputQuery = '';

		foreach ($tables as $table) {
			if (!$table->isTableValid() || $force ) $outputQuery .= $table->getDropTableQuery() . $table->getCreateTableQuery();
		}

		return $outputQuery;
	}

	public function getDropTableQuery(): string {
		return $this->bookingTable->getDropTableQuery() .
		       $this->bookingAttributeTable->getDropTableQuery() .
		       $this->bookingStudentTable->getDropTableQuery() .
		       $this->bookingMaterialTable->getDropTableQuery() .
		       $this->bookingCompanyTable->getDropTableQuery() .
		       $this->bookingAddressTable->getDropTableQuery() .
		       $this->studentExtraFieldTable->getDropTableQuery() .
		       $this->transactionTable->getDropTableQuery() .
		       $this->bookingDayPartLink->getDropTableQuery();
	}

	public function isDatabaseValid(): bool {
		return $this->bookingTable->isTableValid() &&
		       $this->bookingAttributeTable->isTableValid() &&
		       $this->bookingStudentTable->isTableValid() &&
		       $this->bookingMaterialTable->isTableValid() &&
		       $this->bookingCompanyTable->isTableValid() &&
		       $this->bookingAddressTable->isTableValid() &&
		       $this->studentExtraFieldTable->isTableValid() &&
		       $this->transactionTable->isTableValid() &&
		       $this->bookingDayPartLink->isTableValid();
	}

	public function getCountAll(): int {
		return $this->bookingTable->getCountAll();
	}

	public function generatePasswordBooking( string $bookingId ): string {
		try {
			$password = $this->UUIDGenerator->generateUUIDv4();
		} catch (Exception ) {
			$password = $bookingId;
		}

		$this->bookingTable->updateHash($bookingId, $password);
		return $password;
	}

	public function checkPasswordBooking( string $bookingId, string $password ): bool {
		return $this->bookingTable->checkHash($bookingId, $password);
	}

	public function removePasswordBooking( string $bookingId ): void {
		$this->bookingTable->updateHash($bookingId, null);
	}

	public function getTransaction( string $transactionId ): ?Transaction {
		return $this->transactionTable->get($transactionId);
	}

	public function getBookingByTransaction( string $transactionId ): ?Booking {
		$bookingId = $this->bookingTable->getBookingByTransaction($transactionId);
		return $bookingId !== null ? $this->getBooking($bookingId) : null;
	}

	public function removeOldBookings(string $beforeDate):void
	{
		$ids = $this->bookingTable->getOldBookings($beforeDate);

		foreach($ids as $id){
			$this->removeBooking($id);
		}
	}

	public function rebuildTables(): void {
		$this->bookingAddressTable->rebuildTable();
		$this->bookingAttributeTable->rebuildTable();
		$this->bookingCompanyTable->rebuildTable();
		$this->bookingMaterialTable->rebuildTable();
		$this->bookingStudentTable->rebuildTable();
		$this->bookingTable->rebuildTable();
		$this->studentExtraFieldTable->rebuildTable();
		$this->transactionTable->rebuildTable();
		$this->bookingDayPartLink->rebuildTable();
	}

	public function getAlreadyCompletedBooking( BookingForm $bookingForm ): ?Booking {
		$bookingDatabaseId = $this->bookingTable->getIdByNonceAndCourseId( $bookingForm->getNonce(), $bookingForm->getCourseId() );
		if ($bookingDatabaseId === null) return null;

		$booking = $this->getBooking($bookingDatabaseId);
		if ($booking === null) return null;

		if ($booking->getBookingStatus() !== BookingStatus::Successful && $booking->getBookingStatus() !== BookingStatus::Payed)
			return null;

		$databaseForm = $booking->getBookingForm();

		if (count($databaseForm->getStudents()) !== count($bookingForm->getStudents())) return null;
		if (count($databaseForm->getMaterials()) !== count($bookingForm->getMaterials())) return null;
		if (count($databaseForm->getDayParts()) !== count($bookingForm->getDayParts())) return null;

		/** @var BookingStudent $student */
		$studentNames = array_map(fn($student) => $student->getFirstName(), [...$databaseForm->getStudents()]);
		$studentLastNames = array_map(fn($student) => $student->getLastName(), [...$databaseForm->getStudents()]);
		$studentEmails = array_map(fn($student) => $student->getEmail(), [...$databaseForm->getStudents()]);

		foreach ($bookingForm->getStudents() as $student) {
			if (!in_array($student->getFirstName(), $studentNames)) return null;
			if (!in_array($student->getLastName(), $studentLastNames)) return null;
			if (!in_array($student->getEmail(), $studentEmails)) return null;
		}

		$materials = [...$databaseForm->getMaterials()];
		foreach ($bookingForm->getMaterials() as $material) {
			if (!in_array($material, $materials)) return null;
		}

		$dayParts = [...$databaseForm->getDayParts()];
		foreach ($bookingForm->getDayParts() as $dayPart) {
			if (!in_array($dayPart, $dayParts)) return null;
		}

		return $booking;
	}
}