<?php

declare(strict_types=1);

namespace Maestro\Core\Modules\HourRegistration\Services;

use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
use Maestro\Core\Modules\HourRegistration\Enums\HourLineApprovalStatus;
use Maestro\Core\Modules\HourRegistration\Enums\HourRegistrationType;
use Maestro\Core\Modules\HourRegistration\Enums\HourWorkflowStatus;
use Maestro\Core\Modules\HourRegistration\Models\HourTimesheetLine;
use Maestro\Core\Modules\HourRegistration\Models\HourTimesheetWeek;

final class HourLineSyncService
{
    public function __construct(
        private readonly HourLineTargetValidator $targetValidator,
        private readonly HourWeekWorkflowStatusService $workflowStatusService,
    ) {}

    /**
     * @param  list<int>  $removedLineIds
     * @param  list<array<string, mixed>>  $lines
     */
    public function syncWeek(
        HourTimesheetWeek $week,
        array $lines,
        array $removedLineIds,
    ): HourTimesheetWeek {
        abort_unless(Auth::guard()->user(), 403);

        $week->loadMissing(['lines']);

        $this->assertWeekMutableForEmployee($week);

        return DB::transaction(function () use ($week, $lines, $removedLineIds): HourTimesheetWeek {
            HourTimesheetLine::query()
                ->where('hour_timesheet_week_id', $week->id)
                ->whereIn('id', $removedLineIds)
                ->delete();

            foreach ($lines as $row) {
                $this->upsertLine($week, $row);
            }

            $week->refresh()->load(['lines.hourTask']);
            $this->workflowStatusService->refreshAggregateStatus($week);

            return $week->fresh(['lines.hourTask'])->load(['lines.hourTask']);
        });
    }

    private function assertWeekMutableForEmployee(HourTimesheetWeek $week): void
    {
        $user = Auth::guard()->user();
        abort_unless($user, 403);

        if ($week->workflow_status === HourWorkflowStatus::Approved) {
            throw ValidationException::withMessages([
                'week' => ['Approved weeks are locked. An administrator must unlock first.'],
            ]);
        }

        $isOwner = (string) $week->user_id === (string) $user->getAuthIdentifier();

        if (! $isOwner) {
            abort_unless($user->can('hours.admin'), 403);
        }

        abort_unless($user->can('hours.week.manage') || $user->can('hours.admin'), 403);
    }

    /**
     * @param  array<string, mixed>  $row
     */
    private function upsertLine(HourTimesheetWeek $week, array $row): void
    {
        $registrationType = HourRegistrationType::from((string) $row['registration_type']);
        $this->targetValidator->assertValidTargets($registrationType, $row);

        $line = isset($row['id'])
            ? $week->lines()->whereKey($row['id'])->first()
            : null;

        if (isset($row['id']) && $line === null) {
            throw ValidationException::withMessages([
                'lines' => ['Line id '.$row['id'].' does not belong to this week.'],
            ]);
        }

        $line ??= new HourTimesheetLine(['hour_timesheet_week_id' => $week->id]);

        $normDesc = static function (?string $s): string {
            return trim((string) ($s ?? ''));
        };

        $dirtyTargetsOrType = isset($row['id']) && (
            $registrationType !== $line->registration_type
            || (int) ($row['internal_category_id'] ?? 0) !== (int) $line->internal_category_id
            || (int) ($row['customer_id'] ?? 0) !== (int) $line->customer_id
            || (int) ($row['hour_project_id'] ?? 0) !== (int) $line->hour_project_id
            || (int) ($row['hour_task_id'] ?? 0) !== (int) $line->hour_task_id
        );

        $dirtyDescription = isset($row['id'])
            && array_key_exists('description', $row)
            && $normDesc($line->exists ? ($line->description ?? null) : null) !== $normDesc((string) ($row['description'] ?? ''));

        $dirtyDurations = false;
        if (isset($row['id'])) {
            foreach (HourTimesheetLine::WEEKDAY_KEYS as $dayKey) {
                $field = 'duration_'.$dayKey;
                if (isset($row[$field]) && abs((float) $row[$field] - (float) $line->{$field}) > 1e-6) {
                    $dirtyDurations = true;

                    break;
                }
            }
        }

        $line->registration_type = $registrationType;
        $line->internal_category_id = $registrationType === HourRegistrationType::Internal ? (int) $row['internal_category_id'] : null;
        $line->customer_id = $registrationType === HourRegistrationType::Customer ? (int) $row['customer_id'] : null;
        $line->hour_project_id = $registrationType === HourRegistrationType::Project ? (int) $row['hour_project_id'] : null;
        $line->hour_task_id = $registrationType === HourRegistrationType::Task ? (int) $row['hour_task_id'] : null;

        foreach (HourTimesheetLine::WEEKDAY_KEYS as $dayKey) {
            $field = 'duration_'.$dayKey;
            if (array_key_exists($field, $row)) {
                $line->{$field} = round(max(0.0, (float) $row[$field]), 2);
            }
        }

        if (array_key_exists('description', $row)) {
            $t = trim((string) ($row['description'] ?? ''));
            $line->description = $t === '' ? null : mb_substr($t, 0, 500);
        }

        if ($registrationType === HourRegistrationType::Internal) {
            $line->line_approval_status = HourLineApprovalStatus::Approved;
            $line->approved_by_id = null;
            $line->approved_at = now();
            $line->reject_reason = null;
        } else {
            $shouldInvalidate = ! $line->exists || (
                $week->workflow_status !== HourWorkflowStatus::Draft && ($dirtyTargetsOrType || $dirtyDurations || $dirtyDescription)
            );

            if ($shouldInvalidate) {
                $line->line_approval_status = HourLineApprovalStatus::Pending;
                $line->approved_by_id = null;
                $line->approved_at = null;
                $line->reject_reason = null;
            }
        }

        $line->save();
    }
}
