import 'models.dart';

/// Deterministic source of the current instant.
abstract interface class Clock {
  DateTime now();
}

/// Production clock boundary. Domain services should receive this through
/// constructors rather than reading wall time directly.
class SystemClock implements Clock {
  const SystemClock();

  @override
  DateTime now() => DateTime.now();
}

/// Test/application clock with a fixed instant.
class FixedClock implements Clock {
  const FixedClock(this.instant);

  final DateTime instant;

  @override
  DateTime now() => instant;
}

/// Deterministic ID source for application/domain convenience entry points.
abstract interface class IdGenerator {
  String nextId();
}

/// Simple deterministic ID generator useful for tests and local wiring.
class SequentialIdGenerator implements IdGenerator {
  SequentialIdGenerator({
    this.prefix = 'id',
    int start = 1,
  }) : _next = start;

  final String prefix;
  int _next;

  @override
  String nextId() {
    final id = '$prefix-$_next';
    _next += 1;
    return id;
  }
}

/// Calendar date without a time or timezone.
class CivilDate implements Comparable<CivilDate> {
  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.',
      );
    }
  }

  factory CivilDate.fromDateTime(DateTime value) {
    return CivilDate(value.year, value.month, value.day);
  }

  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]),
    );
  }

  final int year;
  final int month;
  final int day;

  int get weekday => DateTime.utc(year, month, day).weekday;

  CivilDate addDays(int days) {
    final shifted = DateTime.utc(year, month, day).add(Duration(days: days));
    return CivilDate(shifted.year, shifted.month, shifted.day);
  }

  String toIsoString() {
    final monthText = month.toString().padLeft(2, '0');
    final dayText = day.toString().padLeft(2, '0');
    return '$year-$monthText-$dayText';
  }

  @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);
  }

  @override
  bool operator ==(Object other) {
    return other is CivilDate &&
        other.year == year &&
        other.month == month &&
        other.day == day;
  }

  @override
  int get hashCode => Object.hash(year, month, day);

  @override
  String toString() => toIsoString();
}

/// Wall-clock time without a date or timezone.
class WallTime implements Comparable<WallTime> {
  WallTime({
    required this.hour,
    required this.minute,
  }) {
    if (hour < 0 || hour >= 24 || minute < 0 || minute >= 60) {
      throw DomainValidationException(
        code: DomainValidationCode.invalidClockTime,
        invalidValue: {'hour': hour, 'minute': minute},
        name: 'WallTime',
        message: 'Wall time must use hour 0-23 and minute 0-59.',
      );
    }
  }

  factory WallTime.fromIsoString(String value) {
    if (!RegExp(r'^\d{2}:\d{2}$').hasMatch(value)) {
      throw DomainValidationException(
        code: DomainValidationCode.invalidClockTime,
        invalidValue: value,
        name: 'WallTime',
        message: 'Wall time must use HH:MM format.',
      );
    }
    final parts = value.split(':');
    return WallTime(hour: int.parse(parts[0]), minute: int.parse(parts[1]));
  }

  final int hour;
  final int minute;

  int get minutesSinceMidnight => hour * 60 + minute;

  String toIsoString() {
    return '${hour.toString().padLeft(2, '0')}:'
        '${minute.toString().padLeft(2, '0')}';
  }

  @override
  int compareTo(WallTime other) {
    return minutesSinceMidnight.compareTo(other.minutesSinceMidnight);
  }

  @override
  bool operator ==(Object other) {
    return other is WallTime && other.hour == hour && other.minute == minute;
  }

  @override
  int get hashCode => Object.hash(hour, minute);

  @override
  String toString() => toIsoString();
}

enum LocalTimeResolution {
  exact,
  shiftedForward,
  repeatedEarlier,
  repeatedLater,
}

enum NonexistentLocalTimePolicy {
  /// Move through a daylight-saving gap to the first valid local instant after it.
  shiftForward,

  /// Reject local times that do not exist in the requested timezone.
  reject,
}

enum RepeatedLocalTimePolicy {
  /// Use the first instant for a repeated local time during a fall-back transition.
  earlier,

  /// Use the second instant for a repeated local time during a fall-back transition.
  later,

  /// Reject local times that occur more than once in the requested timezone.
  reject,
}

class TimeZoneResolutionOptions {
  const TimeZoneResolutionOptions({
    this.nonexistentLocalTimePolicy = NonexistentLocalTimePolicy.shiftForward,
    this.repeatedLocalTimePolicy = RepeatedLocalTimePolicy.earlier,
  });

  final NonexistentLocalTimePolicy nonexistentLocalTimePolicy;
  final RepeatedLocalTimePolicy repeatedLocalTimePolicy;
}

class ResolvedLocalDateTime {
  const ResolvedLocalDateTime({
    required this.date,
    required this.wallTime,
    required this.timeZoneId,
    required this.instant,
    required this.resolution,
    required this.utcOffset,
  });

  final CivilDate date;
  final WallTime wallTime;
  final String timeZoneId;
  final DateTime instant;
  final LocalTimeResolution resolution;
  final Duration utcOffset;
}

/// Boundary for converting local civil date/time values to instants.
abstract interface class TimeZoneResolver {
  ResolvedLocalDateTime resolve({
    required CivilDate date,
    required WallTime wallTime,
    required String timeZoneId,
    TimeZoneResolutionOptions options,
  });
}

/// Resolver for zones whose offset is already known by the application boundary.
class FixedOffsetTimeZoneResolver implements TimeZoneResolver {
  const FixedOffsetTimeZoneResolver.utc() : offset = Duration.zero;

  const FixedOffsetTimeZoneResolver({required this.offset});

  final Duration offset;

  @override
  ResolvedLocalDateTime resolve({
    required CivilDate date,
    required WallTime wallTime,
    required String timeZoneId,
    TimeZoneResolutionOptions options = const TimeZoneResolutionOptions(),
  }) {
    final trimmedZone = timeZoneId.trim();
    if (trimmedZone.isEmpty) {
      throw DomainValidationException(
        code: DomainValidationCode.blankTimeZoneId,
        invalidValue: timeZoneId,
        name: 'timeZoneId',
        message: 'Time-zone id is required.',
      );
    }
    final localAsUtc = DateTime.utc(
      date.year,
      date.month,
      date.day,
      wallTime.hour,
      wallTime.minute,
    );

    return ResolvedLocalDateTime(
      date: date,
      wallTime: wallTime,
      timeZoneId: trimmedZone,
      instant: localAsUtc.subtract(offset),
      resolution: LocalTimeResolution.exact,
      utcOffset: offset,
    );
  }
}
