<?php

declare(strict_types=1);

// SPDX-FileCopyrightText: Chen Asraf <contact@casraf.dev>
// SPDX-License-Identifier: AGPL-3.0-or-later

namespace OCA\Pantry\Service;

use OCA\Pantry\Db\Checklist;
use OCA\Pantry\Db\ChecklistItem;
use OCA\Pantry\Db\ChecklistItemMapper;
use OCA\Pantry\Db\ChecklistMapper;
use OCA\Pantry\Exception\NotFoundException;
use OCP\AppFramework\Db\DoesNotExistException;

class ChecklistService {
	public function __construct(
		private ChecklistMapper $listMapper,
		private ChecklistItemMapper $itemMapper,
		private RecurrenceService $recurrence,
	) {
	}

	// ----- Lists -----

	/**
	 * @return Checklist[]
	 */
	public function listForHouse(int $houseId): array {
		return $this->listMapper->findByHouse($houseId);
	}

	public function getList(int $listId): Checklist {
		try {
			return $this->listMapper->findById($listId);
		} catch (DoesNotExistException) {
			throw new NotFoundException('List not found');
		}
	}

	public function createList(int $houseId, string $name, ?string $description, ?string $icon = null): Checklist {
		$name = trim($name);
		if ($name === '') {
			throw new \InvalidArgumentException('List name cannot be empty');
		}
		$now = time();
		$list = new Checklist();
		$list->setHouseId($houseId);
		$list->setName($name);
		$list->setDescription($description !== null && $description !== '' ? $description : null);
		if ($icon !== null && $icon !== '') {
			$list->setIcon($icon);
		}
		$list->setSortOrder(0);
		$list->setCreatedAt($now);
		$list->setUpdatedAt($now);
		/** @var Checklist $saved */
		$saved = $this->listMapper->insert($list);
		return $saved;
	}

	public function updateList(int $listId, array $patch): Checklist {
		$list = $this->getList($listId);
		if (isset($patch['name'])) {
			$name = trim((string)$patch['name']);
			if ($name === '') {
				throw new \InvalidArgumentException('List name cannot be empty');
			}
			$list->setName($name);
		}
		if (array_key_exists('description', $patch)) {
			$desc = $patch['description'];
			$list->setDescription(is_string($desc) && $desc !== '' ? $desc : null);
		}
		if (isset($patch['icon'])) {
			$icon = trim((string)$patch['icon']);
			if ($icon !== '') {
				$list->setIcon($icon);
			}
		}
		if (isset($patch['sortOrder'])) {
			$list->setSortOrder((int)$patch['sortOrder']);
		}
		$list->setUpdatedAt(time());
		$this->listMapper->update($list);
		return $list;
	}

	public function deleteList(int $listId): void {
		$list = $this->getList($listId);
		$this->itemMapper->deleteByList((int)$list->getId());
		$this->listMapper->delete($list);
	}

	// ----- Items -----

	/**
	 * List items for a list, auto-unchecking any recurring items whose next_due_at has passed.
	 *
	 * @return ChecklistItem[]
	 */
	public function listItems(int $listId, string $sortBy = 'custom', ?int $now = null): array {
		// Eagerly reopen any due recurring items in this list before returning.
		$this->reopenDueItems($now);
		return $this->itemMapper->findByList($listId, $sortBy);
	}

	public function getItem(int $itemId): ChecklistItem {
		try {
			return $this->itemMapper->findById($itemId);
		} catch (DoesNotExistException) {
			throw new NotFoundException('Item not found');
		}
	}

	public function addItem(int $listId, array $data): ChecklistItem {
		// Ensure the list exists.
		$this->getList($listId);

		$name = trim((string)($data['name'] ?? ''));
		if ($name === '') {
			throw new \InvalidArgumentException('Item name cannot be empty');
		}

		$rrule = isset($data['rrule']) && is_string($data['rrule']) && trim($data['rrule']) !== ''
			? trim($data['rrule'])
			: null;
		if ($rrule !== null) {
			$this->recurrence->validate($rrule);
		}

		$now = time();
		$item = new ChecklistItem();
		$item->setListId($listId);
		$item->setName($name);
		$item->setDescription($this->strOrNull($data['description'] ?? null));
		$item->setCategoryId($this->intOrNull($data['categoryId'] ?? null));
		$item->setQuantity($this->strOrNull($data['quantity'] ?? null));
		$item->setDone(false);
		$item->setDoneAt(null);
		$item->setDoneBy(null);
		$item->setRrule($rrule);
		$repeatFromCompletion = !empty($data['repeatFromCompletion']);
		$item->setRepeatFromCompletion($repeatFromCompletion);
		// For fixed-schedule items, compute the first due time immediately.
		if ($rrule !== null && !$repeatFromCompletion) {
			$item->setNextDueAt($this->computeNextDueAt($item, $now)?->getTimestamp());
		} else {
			$item->setNextDueAt(null);
		}
		$item->setImageFileId($this->intOrNull($data['imageFileId'] ?? null));
		$item->setSortOrder(isset($data['sortOrder']) ? (int)$data['sortOrder'] : 0);
		$item->setCreatedAt($now);
		$item->setUpdatedAt($now);
		/** @var ChecklistItem $saved */
		$saved = $this->itemMapper->insert($item);
		return $saved;
	}

	public function updateItem(int $itemId, array $patch): ChecklistItem {
		$item = $this->getItem($itemId);

		if (isset($patch['name'])) {
			$name = trim((string)$patch['name']);
			if ($name === '') {
				throw new \InvalidArgumentException('Item name cannot be empty');
			}
			$item->setName($name);
		}
		if (array_key_exists('description', $patch)) {
			$item->setDescription($this->strOrNull($patch['description']));
		}
		if (array_key_exists('categoryId', $patch)) {
			$item->setCategoryId($this->intOrNull($patch['categoryId']));
		}
		if (array_key_exists('quantity', $patch)) {
			$item->setQuantity($this->strOrNull($patch['quantity']));
		}
		if (array_key_exists('rrule', $patch)) {
			$rrule = $patch['rrule'];
			if ($rrule === null || (is_string($rrule) && trim($rrule) === '')) {
				$item->setRrule(null);
				// Clearing recurrence also clears any scheduled re-open.
				if ($item->getDone()) {
					$item->setNextDueAt(null);
				}
			} else {
				$rrule = trim((string)$rrule);
				$this->recurrence->validate($rrule);
				$item->setRrule($rrule);
			}
		}
		if (array_key_exists('repeatFromCompletion', $patch)) {
			$item->setRepeatFromCompletion((bool)$patch['repeatFromCompletion']);
		}
		if (array_key_exists('imageFileId', $patch)) {
			$item->setImageFileId($this->intOrNull($patch['imageFileId']));
		}
		if (array_key_exists('imageUploadedBy', $patch)) {
			$v = $patch['imageUploadedBy'];
			$item->setImageUploadedBy(is_string($v) && $v !== '' ? $v : null);
		}
		// If already done and rrule or mode changed, recompute next due.
		if ($item->getDone() && $item->getRrule() !== null
			&& (array_key_exists('rrule', $patch) || array_key_exists('repeatFromCompletion', $patch))) {
			$item->setNextDueAt($this->computeNextDueAt($item, time())?->getTimestamp());
		}
		if (isset($patch['sortOrder'])) {
			$item->setSortOrder((int)$patch['sortOrder']);
		}

		$item->setUpdatedAt(time());
		$this->itemMapper->update($item);
		return $item;
	}

	/**
	 * Batch reorder items within a list.
	 *
	 * @param int $listId List id.
	 * @param array<array{id: int, sortOrder: int}> $items Reorder entries.
	 */
	public function reorderItems(int $listId, array $items): void {
		foreach ($items as $entry) {
			$id = (int)($entry['id'] ?? 0);
			$sortOrder = (int)($entry['sortOrder'] ?? 0);
			if ($id <= 0) {
				continue;
			}
			try {
				$item = $this->itemMapper->findById($id);
			} catch (DoesNotExistException) {
				continue;
			}
			if ($item->getListId() !== $listId) {
				continue;
			}
			$item->setSortOrder($sortOrder);
			$item->setUpdatedAt(time());
			$this->itemMapper->update($item);
		}
	}

	public function toggleItem(int $itemId, string $uid, ?int $now = null): ChecklistItem {
		$item = $this->getItem($itemId);
		$now ??= time();

		if (!$item->getDone()) {
			$item->setDone(true);
			$item->setDoneAt($now);
			$item->setDoneBy($uid);
			if ($item->getRrule() !== null) {
				$item->setNextDueAt($this->computeNextDueAt($item, $now)?->getTimestamp());
			}
		} else {
			$item->setDone(false);
			$item->setDoneAt(null);
			$item->setDoneBy(null);
			$item->setNextDueAt(null);
		}
		$item->setUpdatedAt($now);
		$this->itemMapper->update($item);
		return $item;
	}

	/**
	 * Compute the next due time for an item that was just marked done.
	 *
	 * - "from completion" mode: interval counts from now — next occurrence = now + one step.
	 * - "fixed schedule" mode: the schedule is anchored at the item's creation time; next
	 *   occurrence is the first one strictly after now on that anchored series.
	 */
	private function computeNextDueAt(ChecklistItem $item, int $now): ?\DateTimeImmutable {
		$rrule = $item->getRrule();
		if ($rrule === null) {
			return null;
		}
		$nowDt = (new \DateTimeImmutable())->setTimestamp($now);
		if ($item->getRepeatFromCompletion()) {
			return $this->recurrence->computeNextOccurrence($rrule, $nowDt);
		}
		$anchor = (new \DateTimeImmutable())->setTimestamp($item->getCreatedAt() ?: $now);
		return $this->recurrence->nextOccurrenceAfter($rrule, $anchor, $nowDt);
	}

	/**
	 * Reopen all recurring items whose next_due_at has passed.
	 *
	 * Called both lazily from listItems() and periodically by the background job.
	 *
	 * @return ChecklistItem[] The items that were reopened.
	 */
	public function reopenDueItems(?int $now = null): array {
		$now ??= time();
		$items = $this->itemMapper->findDueRecurring($now);
		foreach ($items as $item) {
			$item->setDone(false);
			$item->setDoneAt(null);
			$item->setDoneBy(null);
			if ($item->getRepeatFromCompletion()) {
				// Completion-based: next interval starts when the user checks
				// the item off again, so clear the schedule for now.
				$item->setNextDueAt(null);
			} else {
				// Fixed schedule: immediately compute the next occurrence so the
				// item keeps cycling even if the user never interacts with it.
				$item->setNextDueAt($this->computeNextDueAt($item, $now)?->getTimestamp());
			}
			$item->setUpdatedAt($now);
			$this->itemMapper->update($item);
		}
		return $items;
	}

	/**
	 * Advance fixed-schedule undone items whose next_due_at has passed.
	 *
	 * Unlike reopenDueItems(), these items are already undone — nothing to
	 * reopen. We just bump next_due_at to the next occurrence so the
	 * background job can re-notify on the next cycle.
	 *
	 * @return ChecklistItem[] The items whose schedule was advanced.
	 */
	public function advanceDueReminders(?int $now = null): array {
		$now ??= time();
		$items = $this->itemMapper->findDueFixedScheduleUndone($now);
		foreach ($items as $item) {
			$item->setNextDueAt($this->computeNextDueAt($item, $now)?->getTimestamp());
			$item->setUpdatedAt($now);
			$this->itemMapper->update($item);
		}
		return $items;
	}

	public function deleteItem(int $itemId): void {
		$item = $this->getItem($itemId);
		$this->itemMapper->delete($item);
	}

	private function strOrNull(mixed $v): ?string {
		if (!is_string($v)) {
			return null;
		}
		$t = trim($v);
		return $t === '' ? null : $t;
	}

	private function intOrNull(mixed $v): ?int {
		if ($v === null || $v === '' || $v === false) {
			return null;
		}
		if (is_int($v)) {
			return $v;
		}
		if (is_string($v) && ctype_digit($v)) {
			return (int)$v;
		}
		if (is_numeric($v)) {
			return (int)$v;
		}
		return null;
	}
}
