const std = @import("std");
const abi = @import("abi.zig");
const temporal = @import("temporal.zig");

const PlainDate = @import("PlainDate.zig");
const PlainTime = @import("PlainTime.zig");
const ZonedDateTime = @import("ZonedDateTime.zig");
const Duration = @import("Duration.zig");

const PlainDateTime = @This();

_inner: *abi.c.PlainDateTime,

// Import types from temporal.zig
pub const Unit = temporal.Unit;
pub const RoundingMode = temporal.RoundingMode;
pub const Sign = temporal.Sign;
pub const DifferenceSettings = temporal.DifferenceSettings;
pub const RoundOptions = temporal.RoundingOptions;
// Type definitions for API compatibility
pub const CalendarDisplay = enum {
    auto,
    always,
    never,
    critical,
};

pub const ToStringOptions = struct {
    fractional_second_digits: ?u8 = null,
    smallest_unit: ?Unit = null,
    rounding_mode: ?RoundingMode = null,
    calendar_display: ?CalendarDisplay = null,
};

pub const ToZonedDateTimeOptions = struct {
    timeZone: []const u8,
    disambiguation: ?[]const u8 = null,
};

pub const WithOptions = struct {
    year: ?i32 = null,
    month: ?u8 = null,
    day: ?u8 = null,
    hour: ?u8 = null,
    minute: ?u8 = null,
    second: ?u8 = null,
    millisecond: ?u16 = null,
    microsecond: ?u16 = null,
    nanosecond: ?u16 = null,
};

// Constructor - creates a PlainDateTime with all parameters
pub fn init(
    year_val: i32,
    month_val: u8,
    day_val: u8,
    hour_val: u8,
    minute_val: u8,
    second_val: u8,
    millisecond_val: u16,
    microsecond_val: u16,
    nanosecond_val: u16,
) !PlainDateTime {
    return wrapPlainDateTime(abi.c.temporal_rs_PlainDateTime_try_new(
        year_val,
        month_val,
        day_val,
        hour_val,
        minute_val,
        second_val,
        millisecond_val,
        microsecond_val,
        nanosecond_val,
        abi.c.AnyCalendarKind_Iso,
    ));
}

pub fn initWithCalendar(
    year_val: i32,
    month_val: u8,
    day_val: u8,
    hour_val: u8,
    minute_val: u8,
    second_val: u8,
    millisecond_val: u16,
    microsecond_val: u16,
    nanosecond_val: u16,
    calendar: []const u8,
) !PlainDateTime {
    const cal_view = abi.toDiplomatStringView(calendar);
    const cal_result = abi.c.temporal_rs_AnyCalendarKind_parse_temporal_calendar_string(cal_view);
    const cal_kind = try abi.extractResult(cal_result);
    return wrapPlainDateTime(abi.c.temporal_rs_PlainDateTime_try_new(
        year_val,
        month_val,
        day_val,
        hour_val,
        minute_val,
        second_val,
        millisecond_val,
        microsecond_val,
        nanosecond_val,
        cal_kind,
    ));
}

// Parse from string
pub fn from(s: []const u8) !PlainDateTime {
    return fromUtf8(s);
}

fn fromUtf8(utf8: []const u8) !PlainDateTime {
    const view = abi.toDiplomatStringView(utf8);
    const parsed = abi.c.temporal_rs_ParsedDateTime_from_utf8(view);
    const ptr = try abi.extractResult(parsed);
    return wrapPlainDateTime(abi.c.temporal_rs_PlainDateTime_from_parsed(ptr));
}

fn fromUtf16(utf16: []const u16) !PlainDateTime {
    const view = abi.toDiplomatString16View(utf16);
    const parsed = abi.c.temporal_rs_ParsedDateTime_from_utf16(view);
    const ptr = try abi.extractResult(parsed);
    return wrapPlainDateTime(abi.c.temporal_rs_PlainDateTime_from_parsed(ptr));
}

// Comparison
pub fn compare(a: PlainDateTime, b: PlainDateTime) i8 {
    return abi.c.temporal_rs_PlainDateTime_compare(a._inner, b._inner);
}

pub fn equals(self: PlainDateTime, other: PlainDateTime) bool {
    return compare(self, other) == 0;
}

// Arithmetic
pub fn add(self: PlainDateTime, duration: Duration) !PlainDateTime {
    const overflow_opt = abi.c.ArithmeticOverflow_option{
        .is_ok = true,
        .unnamed_0 = .{ .ok = abi.c.ArithmeticOverflow_Constrain },
    };
    return wrapPlainDateTime(abi.c.temporal_rs_PlainDateTime_add(self._inner, duration._inner, overflow_opt));
}

pub fn subtract(self: PlainDateTime, duration: Duration) !PlainDateTime {
    const overflow_opt = abi.c.ArithmeticOverflow_option{
        .is_ok = true,
        .unnamed_0 = .{ .ok = abi.c.ArithmeticOverflow_Constrain },
    };
    return wrapPlainDateTime(abi.c.temporal_rs_PlainDateTime_subtract(self._inner, duration._inner, overflow_opt));
}

pub fn until(self: PlainDateTime, other: PlainDateTime, options: DifferenceSettings) !Duration {
    const result = abi.c.temporal_rs_PlainDateTime_until(self._inner, other._inner, options.toCApi());
    const ptr = (try abi.extractResult(result)) orelse return abi.TemporalError.Generic;
    return .{ ._inner = ptr };
}

pub fn since(self: PlainDateTime, other: PlainDateTime, options: DifferenceSettings) !Duration {
    const result = abi.c.temporal_rs_PlainDateTime_since(self._inner, other._inner, options.toCApi());
    const ptr = (try abi.extractResult(result)) orelse return abi.TemporalError.Generic;
    return .{ ._inner = ptr };
}

// Rounding
pub fn round(self: PlainDateTime, options: RoundOptions) !PlainDateTime {
    return wrapPlainDateTime(abi.c.temporal_rs_PlainDateTime_round(self._inner, options.toCApi()));
}

// Property accessors - Date fields
pub fn calendarId(self: PlainDateTime, allocator: std.mem.Allocator) ![]u8 {
    const calendar_ptr = abi.c.temporal_rs_PlainDateTime_calendar(self._inner) orelse return error.TemporalError;
    const cal_id_view = abi.c.temporal_rs_Calendar_identifier(calendar_ptr);
    return try allocator.dupe(u8, cal_id_view.data[0..cal_id_view.len]);
}

pub fn day(self: PlainDateTime) u8 {
    return abi.c.temporal_rs_PlainDateTime_day(self._inner);
}

pub fn dayOfWeek(self: PlainDateTime) u16 {
    return abi.c.temporal_rs_PlainDateTime_day_of_week(self._inner);
}

pub fn dayOfYear(self: PlainDateTime) u16 {
    return abi.c.temporal_rs_PlainDateTime_day_of_year(self._inner);
}

pub fn daysInMonth(self: PlainDateTime) u16 {
    return abi.c.temporal_rs_PlainDateTime_days_in_month(self._inner);
}

pub fn daysInWeek(self: PlainDateTime) u16 {
    return abi.c.temporal_rs_PlainDateTime_days_in_week(self._inner);
}

pub fn daysInYear(self: PlainDateTime) u16 {
    return abi.c.temporal_rs_PlainDateTime_days_in_year(self._inner);
}

pub fn month(self: PlainDateTime) u8 {
    return abi.c.temporal_rs_PlainDateTime_month(self._inner);
}

pub fn monthCode(self: PlainDateTime, allocator: std.mem.Allocator) ![]u8 {
    var write = abi.DiplomatWrite.init(allocator);
    defer write.deinit();
    abi.c.temporal_rs_PlainDateTime_month_code(self._inner, &write.inner);
    return try write.toOwnedSlice();
}

pub fn monthsInYear(self: PlainDateTime) u16 {
    return abi.c.temporal_rs_PlainDateTime_months_in_year(self._inner);
}

pub fn year(self: PlainDateTime) i32 {
    return abi.c.temporal_rs_PlainDateTime_year(self._inner);
}

pub fn inLeapYear(self: PlainDateTime) bool {
    return abi.c.temporal_rs_PlainDateTime_in_leap_year(self._inner);
}

pub fn era(self: PlainDateTime, allocator: std.mem.Allocator) !?[]u8 {
    const result = abi.c.temporal_rs_PlainDateTime_era(self._inner);
    if (!result.is_ok) return null;

    var write = abi.DiplomatWrite.init(allocator);
    defer write.deinit();
    abi.c.temporal_rs_Era_to_string(result.unnamed_0.ok, &write.inner);
    return try write.toOwnedSlice();
}

pub fn eraYear(self: PlainDateTime) !?u32 {
    const result = abi.c.temporal_rs_PlainDateTime_era_year(self._inner);
    if (!result.is_ok) return null;
    return result.unnamed_0.ok;
}

pub fn weekOfYear(self: PlainDateTime) !?u16 {
    const result = abi.c.temporal_rs_PlainDateTime_week_of_year(self._inner);
    if (!result.is_ok) return null;
    return result.unnamed_0.ok;
}

pub fn yearOfWeek(self: PlainDateTime) !?i32 {
    const result = abi.c.temporal_rs_PlainDateTime_year_of_week(self._inner);
    if (!result.is_ok) return null;
    return result.unnamed_0.ok;
}

// Property accessors - Time fields
pub fn hour(self: PlainDateTime) u8 {
    return abi.c.temporal_rs_PlainDateTime_hour(self._inner);
}

pub fn minute(self: PlainDateTime) u8 {
    return abi.c.temporal_rs_PlainDateTime_minute(self._inner);
}

pub fn second(self: PlainDateTime) u8 {
    return abi.c.temporal_rs_PlainDateTime_second(self._inner);
}

pub fn millisecond(self: PlainDateTime) u16 {
    return abi.c.temporal_rs_PlainDateTime_millisecond(self._inner);
}

pub fn microsecond(self: PlainDateTime) u16 {
    return abi.c.temporal_rs_PlainDateTime_microsecond(self._inner);
}

pub fn nanosecond(self: PlainDateTime) u16 {
    return abi.c.temporal_rs_PlainDateTime_nanosecond(self._inner);
}

// Modification methods
pub fn with(self: PlainDateTime, partial: WithOptions) !PlainDateTime {
    // Extract fields from partial, or use current values as defaults
    const new_year = partial.year orelse self.year();
    const new_month = partial.month orelse self.month();
    const new_day = partial.day orelse self.day();
    const new_hour = partial.hour orelse self.hour();
    const new_minute = partial.minute orelse self.minute();
    const new_second = partial.second orelse self.second();
    const new_millisecond = partial.millisecond orelse self.millisecond();
    const new_microsecond = partial.microsecond orelse self.microsecond();
    const new_nanosecond = partial.nanosecond orelse self.nanosecond();

    // Preserve calendar
    const calendar_ptr = abi.c.temporal_rs_PlainDateTime_calendar(self._inner) orelse return error.TemporalError;
    const cal_id_view = abi.c.temporal_rs_Calendar_identifier(calendar_ptr);
    const cal_view = abi.c.DiplomatStringView{ .data = cal_id_view.data, .len = cal_id_view.len };
    const cal_result = abi.c.temporal_rs_AnyCalendarKind_parse_temporal_calendar_string(cal_view);
    const cal_kind = try abi.extractResult(cal_result);

    return wrapPlainDateTime(abi.c.temporal_rs_PlainDateTime_try_new(
        new_year,
        new_month,
        new_day,
        new_hour,
        new_minute,
        new_second,
        new_millisecond,
        new_microsecond,
        new_nanosecond,
        cal_kind,
    ));
}

pub fn withCalendar(self: PlainDateTime, calendar: []const u8) !PlainDateTime {
    const cal_view = abi.toDiplomatStringView(calendar);
    const cal_result = abi.c.temporal_rs_AnyCalendarKind_parse_temporal_calendar_string(cal_view);
    const cal_kind = try abi.extractResult(cal_result);
    const ptr = abi.c.temporal_rs_PlainDateTime_with_calendar(self._inner, cal_kind) orelse return abi.TemporalError.Generic;

    return .{ ._inner = ptr };
}

pub fn withPlainTime(self: PlainDateTime, time: ?PlainTime) !PlainDateTime {
    const new_hour: u8 = if (time) |t| t.hour() else 0;
    const new_minute: u8 = if (time) |t| t.minute() else 0;
    const new_second: u8 = if (time) |t| t.second() else 0;
    const new_millisecond: u16 = if (time) |t| t.millisecond() else 0;
    const new_microsecond: u16 = if (time) |t| t.microsecond() else 0;
    const new_nanosecond: u16 = if (time) |t| t.nanosecond() else 0;

    const calendar_ptr = abi.c.temporal_rs_PlainDateTime_calendar(self._inner) orelse return error.TemporalError;
    const cal_id_view = abi.c.temporal_rs_Calendar_identifier(calendar_ptr);
    const cal_view = abi.c.DiplomatStringView{ .data = cal_id_view.data, .len = cal_id_view.len };
    const cal_result = abi.c.temporal_rs_AnyCalendarKind_parse_temporal_calendar_string(cal_view);
    const cal_kind = try abi.extractResult(cal_result);

    return wrapPlainDateTime(abi.c.temporal_rs_PlainDateTime_try_new(
        self.year(),
        self.month(),
        self.day(),
        new_hour,
        new_minute,
        new_second,
        new_millisecond,
        new_microsecond,
        new_nanosecond,
        cal_kind,
    ));
}

// Conversion methods
pub fn toPlainDate(self: PlainDateTime) !PlainDate {
    const ptr = abi.c.temporal_rs_PlainDateTime_to_plain_date(self._inner) orelse return error.TemporalError;

    return .{ ._inner = ptr };
}

pub fn toPlainTime(self: PlainDateTime) !PlainTime {
    const ptr = abi.c.temporal_rs_PlainDateTime_to_plain_time(self._inner) orelse return error.TemporalError;
    return .{ ._inner = ptr };
}

pub fn toZonedDateTime(self: PlainDateTime, options: ToZonedDateTimeOptions) !ZonedDateTime {
    const tz_view = abi.toDiplomatStringView(options.timeZone);
    const tz_result = abi.c.temporal_rs_TimeZone_try_from_str(tz_view);
    const time_zone = try abi.extractResult(tz_result);

    // Convert to PlainDate and PlainTime
    const date = try self.toPlainDate();
    const time = try self.toPlainTime();

    // Use PlainDate's toZonedDateTime with the time component
    const ptr = (try abi.extractResult(abi.c.temporal_rs_PlainDate_to_zoned_date_time(date._inner, time_zone, time._inner))) orelse return abi.TemporalError.Generic;

    return .{ ._inner = ptr };
}

pub fn toString(self: PlainDateTime, allocator: std.mem.Allocator, options: ToStringOptions) ![]u8 {
    var write = abi.DiplomatWrite.init(allocator);
    defer write.deinit();

    const rounding_options = std.mem.zeroes(abi.c.ToStringRoundingOptions);

    const display_cal: abi.c.DisplayCalendar = if (options.calendar_display) |cal| switch (cal) {
        .auto => @intCast(abi.c.DisplayCalendar_Auto),
        .always => @intCast(abi.c.DisplayCalendar_Always),
        .never => @intCast(abi.c.DisplayCalendar_Never),
        .critical => @intCast(abi.c.DisplayCalendar_Critical),
    } else @intCast(abi.c.DisplayCalendar_Auto);

    const result = abi.c.temporal_rs_PlainDateTime_to_ixdtf_string(
        self._inner,
        rounding_options,
        display_cal,
        &write.inner,
    );

    if (!result.is_ok) return error.TemporalError;
    return try write.toOwnedSlice();
}

pub fn toJSON(self: PlainDateTime, allocator: std.mem.Allocator) ![]u8 {
    return try self.toString(allocator, .{});
}

pub fn toLocaleString(self: PlainDateTime, allocator: std.mem.Allocator) []const u8 {
    const s = self.toString(allocator, .{}) catch return "PlainDateTime.toLocaleString error";
    return s;
}

pub fn valueOf(self: PlainDateTime) !void {
    _ = self;
    return error.ComparisonNotSupported;
}

// Helper functions
fn clone(self: PlainDateTime) PlainDateTime {
    return self;
}

pub fn deinit(self: *PlainDateTime) void {
    _ = self;
    // The C API manages memory
}

fn wrapPlainDateTime(res: anytype) !PlainDateTime {
    const ptr = (try abi.extractResult(res)) orelse return abi.TemporalError.Generic;

    return .{ ._inner = ptr };
}

// ---------- Tests ---------------------
test init {
    const dt = try PlainDateTime.init(2024, 1, 15, 14, 30, 45, 123, 456, 789);
    try std.testing.expectEqual(@as(i32, 2024), dt.year());
    try std.testing.expectEqual(@as(u8, 1), dt.month());
    try std.testing.expectEqual(@as(u8, 15), dt.day());
    try std.testing.expectEqual(@as(u8, 14), dt.hour());
    try std.testing.expectEqual(@as(u8, 30), dt.minute());
    try std.testing.expectEqual(@as(u8, 45), dt.second());
}

test from {
    const dt = try PlainDateTime.from("2024-01-15T14:30:45");
    try std.testing.expectEqual(@as(i32, 2024), dt.year());
    try std.testing.expectEqual(@as(u8, 1), dt.month());
    try std.testing.expectEqual(@as(u8, 15), dt.day());
}

test compare {
    const dt1 = try PlainDateTime.init(2024, 1, 15, 14, 30, 0, 0, 0, 0);
    const dt2 = try PlainDateTime.init(2024, 1, 15, 14, 30, 0, 0, 0, 0);
    const dt3 = try PlainDateTime.init(2024, 1, 16, 14, 30, 0, 0, 0, 0);

    try std.testing.expectEqual(@as(i8, 0), PlainDateTime.compare(dt1, dt2));
    try std.testing.expectEqual(@as(i8, -1), PlainDateTime.compare(dt1, dt3));
    try std.testing.expectEqual(@as(i8, 1), PlainDateTime.compare(dt3, dt1));
}

test equals {
    const dt1 = try PlainDateTime.init(2024, 1, 15, 14, 30, 0, 0, 0, 0);
    const dt2 = try PlainDateTime.init(2024, 1, 15, 14, 30, 0, 0, 0, 0);
    const dt3 = try PlainDateTime.init(2024, 1, 16, 14, 30, 0, 0, 0, 0);

    try std.testing.expect(dt1.equals(dt2));
    try std.testing.expect(!dt1.equals(dt3));
}

test add {
    const dt = try PlainDateTime.init(2024, 1, 15, 14, 30, 0, 0, 0, 0);
    const dur = try Duration.from("P1D");
    const result = try dt.add(dur);

    try std.testing.expectEqual(@as(u8, 16), result.day());
}

test subtract {
    const dt = try PlainDateTime.init(2024, 1, 15, 14, 30, 0, 0, 0, 0);
    const dur = try Duration.from("P1D");
    const result = try dt.subtract(dur);

    try std.testing.expectEqual(@as(u8, 14), result.day());
}

test until {
    const dt1 = try PlainDateTime.init(2024, 1, 15, 14, 30, 0, 0, 0, 0);
    const dt2 = try PlainDateTime.init(2024, 1, 16, 14, 30, 0, 0, 0, 0);
    const dur = try dt1.until(dt2, .{});

    try std.testing.expectEqual(@as(i64, 1), dur.days());
}

test since {
    const dt1 = try PlainDateTime.init(2024, 1, 16, 14, 30, 0, 0, 0, 0);
    const dt2 = try PlainDateTime.init(2024, 1, 15, 14, 30, 0, 0, 0, 0);
    const dur = try dt1.since(dt2, .{});

    try std.testing.expectEqual(@as(i64, 1), dur.days());
}

test round {
    const dt = try PlainDateTime.init(2024, 1, 15, 14, 30, 45, 999, 999, 999);
    const rounded = try dt.round(.{ .smallest_unit = .second });

    try std.testing.expectEqual(@as(u8, 46), rounded.second());
    try std.testing.expectEqual(@as(u16, 0), rounded.millisecond());
    try std.testing.expectEqual(@as(u16, 0), rounded.microsecond());
    try std.testing.expectEqual(@as(u16, 0), rounded.nanosecond());
}

test toString {
    const dt = try PlainDateTime.init(2024, 1, 15, 14, 30, 45, 123, 0, 0);
    const str = try dt.toString(std.testing.allocator, .{});
    defer std.testing.allocator.free(str);

    try std.testing.expect(std.mem.indexOf(u8, str, "2024-01-15") != null);
    try std.testing.expect(std.mem.indexOf(u8, str, "14:30:45") != null);
}

test toJSON {
    const dt = try PlainDateTime.init(2024, 1, 15, 14, 30, 45, 0, 0, 0);
    const str = try dt.toJSON(std.testing.allocator);
    defer std.testing.allocator.free(str);

    try std.testing.expect(std.mem.indexOf(u8, str, "2024-01-15") != null);
}

test toLocaleString {
    const dt = try PlainDateTime.init(2024, 1, 15, 14, 30, 45, 0, 0, 0);
    const s_const = dt.toLocaleString(std.testing.allocator);
    const s = @constCast(s_const);
    defer std.testing.allocator.free(s);
    try std.testing.expect(std.mem.indexOf(u8, s, "2024-01-15") != null);
}

test toPlainDate {
    const dt = try PlainDateTime.init(2024, 1, 15, 14, 30, 0, 0, 0, 0);
    const date = try dt.toPlainDate();

    try std.testing.expectEqual(@as(i32, 2024), date.year());
    try std.testing.expectEqual(@as(u8, 1), date.month());
    try std.testing.expectEqual(@as(u8, 15), date.day());
}

test toPlainTime {
    const dt = try PlainDateTime.init(2024, 1, 15, 14, 30, 45, 0, 0, 0);
    const t = try dt.toPlainTime();
    try std.testing.expectEqual(@as(u8, 14), t.hour());
    try std.testing.expectEqual(@as(u8, 30), t.minute());
    try std.testing.expectEqual(@as(u8, 45), t.second());
}

test toZonedDateTime {
    const dt = try PlainDateTime.init(2024, 1, 15, 14, 30, 0, 0, 0, 0);
    const zdt = try dt.toZonedDateTime(.{ .timeZone = "UTC" });

    const pdt = try zdt.toPlainDateTime();
    try std.testing.expectEqual(@as(i32, 2024), pdt.year());
    try std.testing.expectEqual(@as(u8, 1), pdt.month());
    try std.testing.expectEqual(@as(u8, 15), pdt.day());
}

test with {
    const dt = try PlainDateTime.init(2024, 1, 15, 14, 30, 45, 0, 0, 0);
    const result = try dt.with(.{ .day = 20, .hour = 16 });
    try std.testing.expectEqual(@as(i32, 2024), result.year());
    try std.testing.expectEqual(@as(u8, 1), result.month());
    try std.testing.expectEqual(@as(u8, 20), result.day());
    try std.testing.expectEqual(@as(u8, 16), result.hour());
    try std.testing.expectEqual(@as(u8, 30), result.minute());
}

test withCalendar {
    const dt = try PlainDateTime.init(2024, 1, 15, 14, 30, 0, 0, 0, 0);
    const result = try dt.withCalendar("iso8601");

    try std.testing.expectEqual(@as(i32, 2024), result.year());
    const cal_id = try result.calendarId(std.testing.allocator);
    defer std.testing.allocator.free(cal_id);
    try std.testing.expectEqualStrings("iso8601", cal_id);
}

test withPlainTime {
    const dt = try PlainDateTime.init(2024, 1, 15, 14, 30, 0, 0, 0, 0);
    const t = try PlainTime.from("10:15:30");
    const result = try dt.withPlainTime(t);
    try std.testing.expectEqual(@as(u8, 10), result.hour());
    try std.testing.expectEqual(@as(u8, 15), result.minute());
    try std.testing.expectEqual(@as(u8, 30), result.second());
}
