<?php

namespace App\Services;

use App\Models\CategoryModel;
use Throwable;

class CategoryService
{
    public const MSG_REQUIRED   = 'Please enter a category name.';
    public const MSG_LENGTH     = 'Category name must be 100 characters or fewer.';
    public const MSG_DUPLICATE  = 'A category with this name already exists.';
    public const MSG_NOT_FOUND  = 'That category could not be found.';
    public const MSG_HAS_TASKS  = 'This category has tasks. Choose to move them or delete them.';
    public const MSG_NO_TARGET  = 'Select another category to move tasks into.';
    public const MSG_BAD_TARGET = 'Select a valid category you own.';
    public const MSG_SAME       = 'Choose a different category to move the tasks into.';
    public const MSG_NO_OTHER   = 'Create another category before moving these tasks.';
    public const MSG_FAIL       = 'We could not update that category. Please try again.';
    public const MSG_DELETED    = 'Category deleted.';
    public const MSG_CREATED    = 'Category added.';
    public const MSG_UPDATED    = 'Category updated.';

    public const NAME_MAX = 100;

    /** @var callable|null Invoked inside the transaction before commit (tests). */
    public $beforeCommit = null;

    public function __construct(
        private readonly CategoryModel $categories = new CategoryModel(),
    ) {
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function listForUser(int $userId): array
    {
        $rows = $this->categories
            ->where('user_id', $userId)
            ->orderBy('name', 'ASC')
            ->findAll();

        $counts = $this->taskCounts($userId);
        foreach ($rows as &$row) {
            $row['task_count'] = $counts[(int) $row['id']] ?? 0;
        }

        return $rows;
    }

    public function findOwned(int $userId, int $categoryId): ?array
    {
        return $this->categories->findOwned($userId, $categoryId);
    }

    /**
     * @return array{ok: bool, message: string, errors?: array<string, string>, category?: array<string, mixed>}
     */
    public function create(int $userId, string $name): array
    {
        $name   = $this->normalizeName($name);
        $errors = $this->validateName($userId, $name);
        if ($errors !== []) {
            return $this->fail($errors['name'], $errors);
        }

        $id = $this->categories->skipValidation(true)->insert([
            'user_id'   => $userId,
            'name'      => $name,
            'is_active' => 1,
        ], true);

        $row = $this->categories->find($id);
        if ($row === null) {
            return $this->fail(self::MSG_FAIL);
        }

        return ['ok' => true, 'message' => self::MSG_CREATED, 'category' => $row];
    }

    /**
     * @return array{ok: bool, message: string, errors?: array<string, string>}
     */
    public function update(int $userId, int $categoryId, string $name): array
    {
        if ($this->findOwned($userId, $categoryId) === null) {
            return $this->fail(self::MSG_NOT_FOUND);
        }

        $name   = $this->normalizeName($name);
        $errors = $this->validateName($userId, $name, $categoryId);
        if ($errors !== []) {
            return $this->fail($errors['name'], $errors);
        }

        $this->categories->skipValidation(true)->update($categoryId, ['name' => $name]);

        return ['ok' => true, 'message' => self::MSG_UPDATED];
    }

    /**
     * @return array{ok: bool, message: string}
     */
    public function deleteEmpty(int $userId, int $categoryId): array
    {
        if ($this->findOwned($userId, $categoryId) === null) {
            return $this->fail(self::MSG_NOT_FOUND);
        }

        if ($this->taskCount($userId, $categoryId) > 0) {
            return $this->fail(self::MSG_HAS_TASKS);
        }

        return $this->runTransaction(function () use ($categoryId): void {
            $this->categories->delete($categoryId);
        }, self::MSG_DELETED);
    }

    /**
     * @return array{ok: bool, message: string}
     */
    public function moveAndDelete(int $userId, int $sourceId, int $targetId): array
    {
        $source = $this->findOwned($userId, $sourceId);
        if ($source === null) {
            return $this->fail(self::MSG_NOT_FOUND);
        }

        if ($targetId < 1) {
            return $this->fail(self::MSG_NO_TARGET);
        }

        if ($sourceId === $targetId) {
            return $this->fail(self::MSG_SAME);
        }

        $target = $this->findOwned($userId, $targetId);
        if ($target === null) {
            return $this->fail(self::MSG_BAD_TARGET);
        }

        $others = $this->otherCategories($userId, $sourceId);
        if ($others === []) {
            return $this->fail(self::MSG_NO_OTHER);
        }

        return $this->runTransaction(function () use ($userId, $sourceId, $targetId): void {
            db_connect()->table('tasks')
                ->where('user_id', $userId)
                ->where('category_id', $sourceId)
                ->update(['category_id' => $targetId, 'updated_at' => date('Y-m-d H:i:s')]);

            $this->categories->delete($sourceId);
        }, self::MSG_DELETED);
    }

    /**
     * @return array{ok: bool, message: string}
     */
    public function deleteWithTasks(int $userId, int $categoryId): array
    {
        if ($this->findOwned($userId, $categoryId) === null) {
            return $this->fail(self::MSG_NOT_FOUND);
        }

        return $this->runTransaction(function () use ($userId, $categoryId): void {
            $taskIds = $this->taskIdsForCategory($userId, $categoryId);
            if ($taskIds !== []) {
                db_connect()->table('task_occurrences')->whereIn('task_id', $taskIds)->delete();
                db_connect()->table('task_recurrence_rules')->whereIn('task_id', $taskIds)->delete();
                db_connect()->table('tasks')->whereIn('id', $taskIds)->update([
                    'deleted_at' => date('Y-m-d H:i:s'),
                    'updated_at' => date('Y-m-d H:i:s'),
                ]);
            }

            $this->categories->delete($categoryId);
        }, self::MSG_DELETED);
    }

    public function taskCount(int $userId, int $categoryId): int
    {
        return (int) db_connect()->table('tasks')
            ->where('user_id', $userId)
            ->where('category_id', $categoryId)
            ->where('deleted_at', null)
            ->countAllResults();
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function otherCategories(int $userId, int $exceptId): array
    {
        return $this->categories
            ->where('user_id', $userId)
            ->where('id !=', $exceptId)
            ->orderBy('name', 'ASC')
            ->findAll();
    }

    public function normalizeName(string $name): string
    {
        return trim($name);
    }

    /**
     * @return array<string, string>
     */
    public function validateName(int $userId, string $name, ?int $exceptId = null): array
    {
        if ($name === '') {
            return ['name' => self::MSG_REQUIRED];
        }

        if (mb_strlen($name) > self::NAME_MAX) {
            return ['name' => self::MSG_LENGTH];
        }

        if ($this->categories->nameExistsForUser($userId, $name, $exceptId)) {
            return ['name' => self::MSG_DUPLICATE];
        }

        return [];
    }

    /**
     * @return array<int, int>
     */
    private function taskCounts(int $userId): array
    {
        $rows = db_connect()->table('tasks')
            ->select('category_id, COUNT(*) AS task_count')
            ->where('user_id', $userId)
            ->where('deleted_at', null)
            ->groupBy('category_id')
            ->get()
            ->getResultArray();

        $counts = [];
        foreach ($rows as $row) {
            $counts[(int) $row['category_id']] = (int) $row['task_count'];
        }

        return $counts;
    }

    /**
     * @return list<int>
     */
    private function taskIdsForCategory(int $userId, int $categoryId): array
    {
        $rows = db_connect()->table('tasks')
            ->select('id')
            ->where('user_id', $userId)
            ->where('category_id', $categoryId)
            ->get()
            ->getResultArray();

        return array_map(static fn (array $row): int => (int) $row['id'], $rows);
    }

    /**
     * @param callable(): void $work
     *
     * @return array{ok: bool, message: string}
     */
    private function runTransaction(callable $work, string $success): array
    {
        $db = db_connect();
        $db->transException(true);
        $db->transStart();

        try {
            $work();
            if (is_callable($this->beforeCommit)) {
                ($this->beforeCommit)();
            }
            $db->transComplete();
        } catch (Throwable $exception) {
            if ($db->transStatus()) {
                $db->transRollback();
            }
            log_message('error', 'Category transaction failed: {0}', [$exception->getMessage()]);

            return $this->fail(self::MSG_FAIL);
        }

        if (! $db->transStatus()) {
            return $this->fail(self::MSG_FAIL);
        }

        return ['ok' => true, 'message' => $success];
    }

    /**
     * @param array<string, string> $errors
     *
     * @return array{ok: bool, message: string, errors?: array<string, string>}
     */
    private function fail(string $message, array $errors = []): array
    {
        $result = ['ok' => false, 'message' => $message];
        if ($errors !== []) {
            $result['errors'] = $errors;
        }

        return $result;
    }
}
