import Foundation

enum GoogleCalendarClientError: Error {
    case invalidResponse
    case httpStatus(Int, String)
    case decodeFailed(String)
}

final class GoogleCalendarClient {
    struct Options: Sendable {
        var daysBack: Int
        var daysAhead: Int
        var maxResults: Int
        var includeNonMeetEvents: Bool

        init(daysBack: Int = 1, daysAhead: Int = 180, maxResults: Int = 200, includeNonMeetEvents: Bool = true) {
            self.daysBack = daysBack
            self.daysAhead = daysAhead
            self.maxResults = maxResults
            self.includeNonMeetEvents = includeNonMeetEvents
        }
    }

    private let session: URLSession

    init(session: URLSession = .shared) {
        self.session = session
    }

    func fetchUpcomingMeetings(accessToken: String) async throws -> [ScheduledMeeting] {
        try await fetchUpcomingMeetings(accessToken: accessToken, options: Options())
    }

    func fetchUpcomingMeetings(accessToken: String, options: Options) async throws -> [ScheduledMeeting] {
        let now = Date()
        let start = Calendar.current.date(byAdding: .day, value: -max(0, options.daysBack), to: now) ?? now.addingTimeInterval(-24 * 60 * 60)
        let end = Calendar.current.date(byAdding: .day, value: max(1, options.daysAhead), to: now) ?? now.addingTimeInterval(180 * 24 * 60 * 60)
        let formatter = ISO8601DateFormatter()
        formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
        let totalLimit = max(1, options.maxResults)
        let pageSize = min(250, totalLimit)
        var nextPageToken: String?
        var meetings: [ScheduledMeeting] = []

        repeat {
            var components = URLComponents(string: "https://www.googleapis.com/calendar/v3/calendars/primary/events")!
            var queryItems = [
                URLQueryItem(name: "timeMin", value: formatter.string(from: start)),
                URLQueryItem(name: "timeMax", value: formatter.string(from: end)),
                URLQueryItem(name: "singleEvents", value: "true"),
                URLQueryItem(name: "orderBy", value: "startTime"),
                URLQueryItem(name: "maxResults", value: String(pageSize)),
                URLQueryItem(name: "conferenceDataVersion", value: "1")
            ]
            if let nextPageToken {
                queryItems.append(URLQueryItem(name: "pageToken", value: nextPageToken))
            }
            components.queryItems = queryItems

            var request = URLRequest(url: components.url!)
            request.httpMethod = "GET"
            request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")

            let (data, response) = try await session.data(for: request)
            guard let http = response as? HTTPURLResponse else { throw GoogleCalendarClientError.invalidResponse }
            guard (200..<300).contains(http.statusCode) else {
                let body = String(data: data, encoding: .utf8) ?? "<no body>"
                throw GoogleCalendarClientError.httpStatus(http.statusCode, body)
            }

            let decoded: EventsList
            do {
                decoded = try JSONDecoder().decode(EventsList.self, from: data)
            } catch {
                let raw = String(data: data, encoding: .utf8) ?? "<unreadable body>"
                throw GoogleCalendarClientError.decodeFailed(raw)
            }

            let pageMeetings: [ScheduledMeeting] = decoded.items.compactMap { item in
            let title = item.summary?.trimmingCharacters(in: .whitespacesAndNewlines)
            let subtitle = item.organizer?.displayName ?? item.organizer?.email

            guard let start = item.start?.resolvedDate,
                  let end = item.end?.resolvedDate else { return nil }

            let isAllDay = item.start?.date != nil

            let meetURL: URL? = {
                if let hangout = item.hangoutLink, let u = URL(string: hangout) { return u }
                let entry = item.conferenceData?.entryPoints?.first(where: { $0.entryPointType == "video" && ($0.uri?.contains("meet.google.com") ?? false) })
                if let uri = entry?.uri, let u = URL(string: uri) { return u }
                if options.includeNonMeetEvents, let htmlLink = item.htmlLink, let u = URL(string: htmlLink) { return u }
                return nil
            }()

            if meetURL == nil, options.includeNonMeetEvents == false { return nil }
            guard let meetURL else { return nil }

            return ScheduledMeeting(
                id: item.id ?? UUID().uuidString,
                title: (title?.isEmpty == false) ? title! : "Untitled meeting",
                subtitle: subtitle,
                startDate: start,
                endDate: end,
                meetURL: meetURL,
                isAllDay: isAllDay
            )
            }

            meetings.append(contentsOf: pageMeetings)
            nextPageToken = decoded.nextPageToken
        } while nextPageToken != nil && meetings.count < totalLimit

        if meetings.count > totalLimit {
            meetings = Array(meetings.prefix(totalLimit))
        }
        return meetings
    }

    func createEvent(accessToken: String,
                     title: String,
                     description: String?,
                     start: Date,
                     end: Date,
                     timeZone: TimeZone = .current) async throws -> ScheduledMeeting {
        var components = URLComponents(string: "https://www.googleapis.com/calendar/v3/calendars/primary/events")!
        components.queryItems = [
            URLQueryItem(name: "conferenceDataVersion", value: "1")
        ]

        let requestID = UUID().uuidString
        let body = CreateEventRequest(
            summary: title,
            description: description,
            start: CreateEventRequest.EventDateTime(dateTime: ISO8601DateFormatter.fractional.string(from: start), timeZone: timeZone.identifier),
            end: CreateEventRequest.EventDateTime(dateTime: ISO8601DateFormatter.fractional.string(from: end), timeZone: timeZone.identifier),
            conferenceData: CreateEventRequest.ConferenceData(
                createRequest: CreateEventRequest.CreateRequest(
                    requestId: requestID,
                    conferenceSolutionKey: CreateEventRequest.ConferenceSolutionKey(type: "hangoutsMeet")
                )
            )
        )

        var request = URLRequest(url: components.url!)
        request.httpMethod = "POST"
        request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
        request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
        request.httpBody = try JSONEncoder().encode(body)

        let (data, response) = try await session.data(for: request)
        guard let http = response as? HTTPURLResponse else { throw GoogleCalendarClientError.invalidResponse }
        guard (200..<300).contains(http.statusCode) else {
            let raw = String(data: data, encoding: .utf8) ?? "<no body>"
            throw GoogleCalendarClientError.httpStatus(http.statusCode, raw)
        }

        let created: EventItem
        do {
            created = try JSONDecoder().decode(EventItem.self, from: data)
        } catch {
            let raw = String(data: data, encoding: .utf8) ?? "<unreadable body>"
            throw GoogleCalendarClientError.decodeFailed(raw)
        }

        guard let startDate = created.start?.resolvedDate,
              let endDate = created.end?.resolvedDate else {
            throw GoogleCalendarClientError.decodeFailed("Created event missing start/end.")
        }

        let isAllDay = created.start?.date != nil
        let meetURL: URL? = {
            if let hangout = created.hangoutLink, let u = URL(string: hangout) { return u }
            let entry = created.conferenceData?.entryPoints?.first(where: { $0.entryPointType == "video" && ($0.uri?.contains("meet.google.com") ?? false) })
            if let uri = entry?.uri, let u = URL(string: uri) { return u }
            if let htmlLink = created.htmlLink, let u = URL(string: htmlLink) { return u }
            return nil
        }()
        guard let meetURL else {
            throw GoogleCalendarClientError.decodeFailed("Created event missing Meet/HTML link.")
        }

        let cleanedTitle = created.summary?.trimmingCharacters(in: .whitespacesAndNewlines)
        let subtitle = created.organizer?.displayName ?? created.organizer?.email
        return ScheduledMeeting(
            id: created.id ?? UUID().uuidString,
            title: (cleanedTitle?.isEmpty == false) ? cleanedTitle! : "Untitled meeting",
            subtitle: subtitle,
            startDate: startDate,
            endDate: endDate,
            meetURL: meetURL,
            isAllDay: isAllDay
        )
    }
}

extension GoogleCalendarClientError: LocalizedError {
    var errorDescription: String? {
        switch self {
        case .invalidResponse:
            return "Google Calendar returned an invalid response."
        case let .httpStatus(status, body):
            return "Google Calendar API error (\(status)): \(body)"
        case let .decodeFailed(raw):
            return "Failed to parse Google Calendar events: \(raw)"
        }
    }
}

// MARK: - Calendar API models

private struct EventsList: Decodable {
    let items: [EventItem]
    let nextPageToken: String?
}

private struct EventItem: Decodable {
    let id: String?
    let summary: String?
    let hangoutLink: String?
    let htmlLink: String?
    let organizer: Organizer?
    let start: EventDateTime?
    let end: EventDateTime?
    let conferenceData: ConferenceData?
}

private struct Organizer: Decodable {
    let displayName: String?
    let email: String?
}

private struct EventDateTime: Decodable {
    let dateTime: String?
    let date: String?
    let timeZone: String?

    var resolvedDate: Date? {
        if let dateTime, let parsed = Self.parseDateTime(dateTime, timeZone: timeZone) {
            return parsed
        }
        if let date, let parsed = DateFormatter.googleAllDay.date(from: date) {
            return parsed
        }
        return nil
    }

    private static func parseDateTime(_ raw: String, timeZone: String?) -> Date? {
        if let dt = ISO8601DateFormatter.fractional.date(from: raw) ?? ISO8601DateFormatter.nonFractional.date(from: raw) {
            return dt
        }

        // Some Calendar payloads provide dateTime without explicit zone and separate timeZone field.
        let formatter = DateFormatter()
        formatter.calendar = Calendar(identifier: .gregorian)
        formatter.locale = Locale(identifier: "en_US_POSIX")
        formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
        if let timeZone, let tz = TimeZone(identifier: timeZone) {
            formatter.timeZone = tz
        } else {
            formatter.timeZone = TimeZone.current
        }
        return formatter.date(from: raw)
    }
}

private struct ConferenceData: Decodable {
    let entryPoints: [EntryPoint]?
}

private struct EntryPoint: Decodable {
    let entryPointType: String?
    let uri: String?
}

private struct CreateEventRequest: Encodable {
    struct EventDateTime: Encodable {
        let dateTime: String
        let timeZone: String
    }

    struct ConferenceSolutionKey: Encodable {
        let type: String
    }

    struct CreateRequest: Encodable {
        let requestId: String
        let conferenceSolutionKey: ConferenceSolutionKey
    }

    struct ConferenceData: Encodable {
        let createRequest: CreateRequest
    }

    let summary: String
    let description: String?
    let start: EventDateTime
    let end: EventDateTime
    let conferenceData: ConferenceData
}

private extension ISO8601DateFormatter {
    static let fractional: ISO8601DateFormatter = {
        let f = ISO8601DateFormatter()
        f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
        return f
    }()

    static let nonFractional: ISO8601DateFormatter = {
        let f = ISO8601DateFormatter()
        f.formatOptions = [.withInternetDateTime]
        return f
    }()
}

private extension DateFormatter {
    static let googleAllDay: DateFormatter = {
        let f = DateFormatter()
        f.calendar = Calendar(identifier: .gregorian)
        f.locale = Locale(identifier: "en_US_POSIX")
        f.timeZone = TimeZone(secondsFromGMT: 0)
        f.dateFormat = "yyyy-MM-dd"
        return f
    }()
}

