<?php

declare(strict_types=1);

namespace App\Services;

use App\Core\Database;
use App\Models\AppointmentModel;
use DateInterval;
use DateTimeImmutable;

/**
 * AppointmentService
 *
 * Booking a single appointment is a plain AppointmentModel::insert().
 * What actually needs dedicated logic:
 *   - Availability: refusing to double-book a staff member.
 *   - Recurrence: expanding a recurrence_rule into real, individually-
 *     manageable appointment rows (never a virtual/computed occurrence
 *     — see AppointmentModel's docblock on parent_appointment_id).
 *   - Status transitions (reschedule/cancel/complete/no-show) each
 *     have their own small rules (e.g. a cancelled series occurrence
 *     shouldn't resurrect itself if the parent regenerates).
 */
final class AppointmentService
{
    private const MAX_GENERATED_OCCURRENCES = 52; // safety cap regardless of `until` date

    public function __construct(private readonly AppointmentModel $appointments = new AppointmentModel())
    {
    }

    /**
     * @return array{success:bool, message:string, id?:int, conflict?:bool}
     */
    public function book(array $data, ?int $actingUserId = null): array
    {
        if (!empty($data['assigned_staff_id']) && $this->appointments->hasConflict(
            (int) $data['assigned_staff_id'],
            $data['scheduled_start'],
            $data['scheduled_end']
        )) {
            return ['success' => false, 'message' => 'This staff member already has an appointment during that time.', 'conflict' => true];
        }

        $recurrence = $data['recurrence_rule'] ?? null;
        unset($data['recurrence_rule']);

        $id = Database::transaction(function () use ($data, $recurrence, $actingUserId) {
            $data['is_recurring_parent'] = $recurrence !== null ? 1 : 0;
            $data['recurrence_rule'] = $recurrence !== null ? json_encode($recurrence) : null;

            $parentId = $this->appointments->insert($data, $actingUserId);

            if ($recurrence !== null) {
                $this->generateOccurrences($parentId, $data, $recurrence, $actingUserId);
            }

            ActivityLogger::record(
                $data['customer_id'] ? 'customer' : 'lead',
                (int) ($data['customer_id'] ?: $data['lead_id']),
                'appointment_booked',
                'Appointment scheduled',
                ['appointment_id' => $parentId]
            );

            return $parentId;
        });

        return ['success' => true, 'message' => 'Appointment booked.', 'id' => $id];
    }

    /**
     * Expands {frequency, interval, until, days_of_week?} into real
     * appointment rows linked back to the parent. `frequency` is
     * 'daily' | 'weekly' | 'biweekly' | 'monthly'; `until` is a
     * Y-m-d date; `days_of_week` (ISO 1=Mon..7=Sun) only applies to
     * 'weekly'/'biweekly' and lets one rule generate e.g. "every
     * Tue and Thu."
     */
    private function generateOccurrences(int $parentId, array $parentData, array $recurrence, ?int $actingUserId): void
    {
        $frequency = $recurrence['frequency'] ?? 'weekly';
        $interval = max(1, (int) ($recurrence['interval'] ?? 1));
        $until = new DateTimeImmutable($recurrence['until'] ?? '+3 months');
        $daysOfWeek = $recurrence['days_of_week'] ?? null;

        $start = new DateTimeImmutable($parentData['scheduled_start']);
        $end = new DateTimeImmutable($parentData['scheduled_end']);
        $duration = $start->diff($end);

        $step = match ($frequency) {
            'daily' => new DateInterval("P{$interval}D"),
            'weekly' => new DateInterval('P' . (7 * $interval) . 'D'),
            'biweekly' => new DateInterval('P' . (14 * $interval) . 'D'),
            'monthly' => new DateInterval("P{$interval}M"),
            default => new DateInterval('P7D'),
        };

        $occurrenceStart = $start;
        $count = 0;

        while ($count < self::MAX_GENERATED_OCCURRENCES) {
            $occurrenceStart = $occurrenceStart->add($step);

            if ($occurrenceStart > $until) {
                break;
            }

            // days_of_week is only meaningful for weekly/biweekly and
            // is intentionally not combined with monthly in this
            // implementation — a monthly rule always repeats on the
            // same day-of-month as the parent.
            if ($daysOfWeek !== null && in_array($frequency, ['weekly', 'biweekly'], true)) {
                if (!in_array((int) $occurrenceStart->format('N'), $daysOfWeek, true)) {
                    continue;
                }
            }

            $occurrenceEnd = $occurrenceStart->add($duration);

            if (!empty($parentData['assigned_staff_id']) && $this->appointments->hasConflict(
                (int) $parentData['assigned_staff_id'],
                $occurrenceStart->format('Y-m-d H:i:s'),
                $occurrenceEnd->format('Y-m-d H:i:s')
            )) {
                continue; // skip conflicting occurrences rather than failing the whole series
            }

            $occurrenceData = $parentData;
            $occurrenceData['parent_appointment_id'] = $parentId;
            $occurrenceData['is_recurring_parent'] = 0;
            $occurrenceData['recurrence_rule'] = null;
            $occurrenceData['scheduled_start'] = $occurrenceStart->format('Y-m-d H:i:s');
            $occurrenceData['scheduled_end'] = $occurrenceEnd->format('Y-m-d H:i:s');

            $this->appointments->insert($occurrenceData, $actingUserId);
            $count++;
        }
    }

    public function reschedule(int $id, string $newStart, string $newEnd, int $actingUserId): array
    {
        $appointment = $this->appointments->find($id);
        if ($appointment === null) {
            return ['success' => false, 'message' => 'Appointment not found.'];
        }

        if (!empty($appointment['assigned_staff_id']) && $this->appointments->hasConflict(
            (int) $appointment['assigned_staff_id'],
            $newStart,
            $newEnd,
            excludeId: $id
        )) {
            return ['success' => false, 'message' => 'That staff member has a conflicting appointment at the new time.'];
        }

        $this->appointments->update($id, [
            'scheduled_start' => $newStart,
            'scheduled_end' => $newEnd,
            'reminder_sent_at' => null, // a rescheduled appointment should get a fresh reminder
        ], $actingUserId);

        ActivityLogger::logAction('Appointment rescheduled', 'user_action', 'appointment', $id);

        return ['success' => true, 'message' => 'Appointment rescheduled.'];
    }

    public function cancel(int $id, string $reason, int $actingUserId): void
    {
        $this->appointments->update($id, ['status' => 'cancelled', 'cancelled_reason' => $reason], $actingUserId);
        ActivityLogger::logAction('Appointment cancelled', 'user_action', 'appointment', $id);
    }

    public function markCompleted(int $id, int $actingUserId): void
    {
        $this->appointments->update($id, ['status' => 'completed'], $actingUserId);
        ActivityLogger::logAction('Appointment marked completed', 'user_action', 'appointment', $id);

        $appointment = $this->appointments->find($id);
        if ($appointment && $appointment['customer_id']) {
            (new \App\Models\CustomerModel())->recalculateLifetimeValue((int) $appointment['customer_id']);
            (new AutomationEngine())->fire('appointment_completed', 'customer', (int) $appointment['customer_id']);
        }
    }

    public function markMissed(int $id, int $actingUserId): void
    {
        $this->appointments->update($id, ['status' => 'no_show'], $actingUserId);
        ActivityLogger::logAction('Appointment marked as missed (no-show)', 'user_action', 'appointment', $id);
    }

    /** Sends reminders for anything due within the window and marks each as sent — call from a scheduled task (e.g. every 5 minutes via cron). */
    public function sendDueReminders(int $withinMinutes = 15): int
    {
        $due = $this->appointments->dueForReminder($withinMinutes);
        $mailer = new MailService();
        $sent = 0;

        foreach ($due as $appt) {
            if (!empty($appt['customer_email'])) {
                $mailer->send(
                    $appt['customer_email'],
                    $appt['customer_name'] ?? '',
                    'Appointment Reminder — A&B Hub',
                    "<p>This is a reminder of your upcoming appointment on "
                    . date('l, F j \a\t g:i A', strtotime($appt['scheduled_start'])) . '.</p>'
                );
                $sent++;
            }

            $this->appointments->markReminderSent((int) $appt['id']);
        }

        return $sent;
    }
}