// SPDX-FileCopyrightText: 2026 FocusFlow contributors
// SPDX-License-Identifier: AGPL-3.0-only

part of '../../time_contracts.dart';

/// Calendar date without a time or timezone.
class CivilDate implements Comparable<CivilDate> {
  /// Creates a `CivilDate` instance with the values required by its invariants.
  /// Callers should pass already-normalized scheduler data because validation and defaulting belong at the model or service boundary, not in arbitrary UI code.
  CivilDate(this.year, this.month, this.day) {
    final normalized = DateTime.utc(year, month, day);
    if (normalized.year != year ||
        normalized.month != month ||
        normalized.day != day) {
      throw DomainValidationException(
        code: DomainValidationCode.invalidCivilDate,
        invalidValue: {'year': year, 'month': month, 'day': day},
        name: 'CivilDate',
        message: 'Civil date must be a valid calendar date.',
      );
    }
  }

  /// Creates a `CivilDate.fromDateTime` instance with the values required by its invariants.
  /// Callers should pass already-normalized scheduler data because validation and defaulting belong at the model or service boundary, not in arbitrary UI code.
  factory CivilDate.fromDateTime(DateTime value) {
    return CivilDate(value.year, value.month, value.day);
  }

  /// Creates a `CivilDate.fromIsoString` instance with the values required by its invariants.
  /// Callers should pass already-normalized scheduler data because validation and defaulting belong at the model or service boundary, not in arbitrary UI code.
  factory CivilDate.fromIsoString(String value) {
    if (!RegExp(r'^\d{4}-\d{2}-\d{2}$').hasMatch(value)) {
      throw DomainValidationException(
        code: DomainValidationCode.invalidCivilDate,
        invalidValue: value,
        name: 'CivilDate',
        message: 'Civil date must use YYYY-MM-DD format.',
      );
    }
    final parts = value.split('-');
    return CivilDate(
      int.parse(parts[0]),
      int.parse(parts[1]),
      int.parse(parts[2]),
    );
  }

  /// Stores the `year` value for this object.
  /// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
  final int year;

  /// Stores the `month` value for this object.
  /// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
  final int month;

  /// Stores the `day` value for this object.
  /// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
  final int day;

  /// Returns the derived `weekday` value for this object.
  /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated.
  int get weekday => DateTime.utc(year, month, day).weekday;

  /// Performs the `addDays` behavior for this scheduler component.
  /// The method provides a named entry point for this operation so callers do not need to know the lower-level state or persistence steps involved.
  CivilDate addDays(int days) {
    final shifted = DateTime.utc(year, month, day).add(Duration(days: days));
    return CivilDate(shifted.year, shifted.month, shifted.day);
  }

  /// Converts scheduler data for `toIsoString` without changing the source object.
  /// Conversion helpers keep persistence and export shapes explicit so domain models do not depend on storage-specific details.
  String toIsoString() {
    final monthText = month.toString().padLeft(2, '0');
    final dayText = day.toString().padLeft(2, '0');
    return '$year-$monthText-$dayText';
  }

  /// Performs the `compareTo` behavior for this scheduler component.
  /// The method provides a named entry point for this operation so callers do not need to know the lower-level state or persistence steps involved.
  @override
  int compareTo(CivilDate other) {
    final yearComparison = year.compareTo(other.year);
    if (yearComparison != 0) {
      return yearComparison;
    }
    final monthComparison = month.compareTo(other.month);
    if (monthComparison != 0) {
      return monthComparison;
    }
    return day.compareTo(other.day);
  }

  /// Performs the `operator ==` behavior for this scheduler component.
  /// The method provides a named entry point for this operation so callers do not need to know the lower-level state or persistence steps involved.
  @override
  bool operator ==(Object other) {
    return other is CivilDate &&
        other.year == year &&
        other.month == month &&
        other.day == day;
  }

  /// Returns the derived `hashCode` value for this object.
  /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated.
  @override
  int get hashCode => Object.hash(year, month, day);

  /// Converts scheduler data for `toString` without changing the source object.
  /// Conversion helpers keep persistence and export shapes explicit so domain models do not depend on storage-specific details.
  @override
  String toString() => toIsoString();
}
