import Foundation

/// Parses a flight-history CSV export into LoggedFlight candidates.
/// Today the only format we detect is Southwest's PNR-level export
/// (the one with columns like `Flt No`, `ORG`, `DST`, `Dep Date`,
/// `OPNG Flt`), which is what the user's existing log is in. Adding
/// another format is mechanically just another `Format` case + a
/// `parseSouthwest`-style mapper.
struct CSVFlightImporter {
    enum Format: String, CaseIterable {
        case southwest      // SWA PNR export
        case unknown
    }

    struct ParsedFlight {
        let flightDate: Date
        let scheduledDeparture: Date?
        let carrierIATA: String?
        let carrierICAO: String?
        let flightNumber: String?
        let departureIATA: String
        let arrivalIATA: String
        let pnr: String?
    }

    enum ImportError: LocalizedError {
        case unsupportedFormat
        case empty
        case parseFailed(String)
        var errorDescription: String? {
            switch self {
            case .unsupportedFormat: return "This CSV's column layout isn't one we know yet."
            case .empty:             return "The file is empty."
            case .parseFailed(let s): return "Couldn't parse the file: \(s)"
            }
        }
    }

    // MARK: - Entry

    static func parse(_ data: Data) throws -> [ParsedFlight] {
        guard let text = String(data: data, encoding: .utf8)
                ?? String(data: data, encoding: .isoLatin1) else {
            throw ImportError.parseFailed("not text")
        }
        let rows = parseRows(text)
        guard let header = rows.first, rows.count > 1 else {
            throw ImportError.empty
        }
        switch detect(header: header) {
        case .southwest:
            return try parseSouthwest(rows: Array(rows.dropFirst()), header: header)
        case .unknown:
            throw ImportError.unsupportedFormat
        }
    }

    static func detect(header: [String]) -> Format {
        let normalized = Set(header.map { $0.trimmingCharacters(in: .whitespaces).lowercased() })
        let swKeys: Set<String> = ["flt no", "org", "dst", "dep date", "opng flt"]
        if swKeys.isSubset(of: normalized) {
            return .southwest
        }
        return .unknown
    }

    // MARK: - Southwest mapper

    private static func parseSouthwest(rows: [[String]], header: [String]) throws -> [ParsedFlight] {
        let index = Dictionary(uniqueKeysWithValues: header.enumerated().map {
            ($1.trimmingCharacters(in: .whitespaces).lowercased(), $0)
        })
        func col(_ row: [String], _ key: String) -> String? {
            guard let i = index[key], i < row.count else { return nil }
            let v = row[i].trimmingCharacters(in: .whitespaces)
            return v.isEmpty ? nil : v
        }

        let depFmt = DateFormatter()
        depFmt.dateFormat = "MM/dd/yyyy h:mm a"
        depFmt.locale = Locale(identifier: "en_US_POSIX")
        depFmt.timeZone = TimeZone(identifier: "UTC")  // No tz in the file — store as UTC so daily aggregation is stable.

        var out: [ParsedFlight] = []
        out.reserveCapacity(rows.count)
        for row in rows {
            guard let depRaw  = col(row, "dep date"),
                  let scheduledDep = depFmt.date(from: depRaw),
                  let org = col(row, "org"),
                  let dst = col(row, "dst")
            else { continue }
            let flightNum = col(row, "flt no")
            let pnr = col(row, "pnr no")
            // OPNG Flt is "WN1484"; the leading 2 letters are the
            // marketing carrier. Default to WN since every row in the
            // SW export is Southwest.
            let opng = col(row, "opng flt") ?? ""
            let carrierIATA = String(opng.prefix(while: { $0.isLetter }))
                .uppercased()
                .nonEmpty() ?? "WN"
            let carrierICAO: String = {
                switch carrierIATA {
                case "WN": return "SWA"
                default:   return AircraftRegistry.shared.lookup(iata: carrierIATA)?.icao ?? carrierIATA
                }
            }()

            // Strip the day-of so we can dedupe across the user's
            // existing manual entries without worrying about UTC roll.
            let day = Calendar(identifier: .gregorian).startOfDay(for: scheduledDep)

            out.append(ParsedFlight(
                flightDate: day,
                scheduledDeparture: scheduledDep,
                carrierIATA: carrierIATA,
                carrierICAO: carrierICAO,
                flightNumber: flightNum,
                departureIATA: org.uppercased(),
                arrivalIATA: dst.uppercased(),
                pnr: pnr
            ))
        }
        return out
    }

    // MARK: - CSV parser
    //
    // Simple state machine that handles RFC 4180 basics: quoted
    // fields, embedded commas inside quotes, and "" → " escaping.
    // Mac-style and Windows line endings both work because we strip
    // CR before splitting on LF.

    private static func parseRows(_ text: String) -> [[String]] {
        let normalized = text.replacingOccurrences(of: "\r\n", with: "\n")
                             .replacingOccurrences(of: "\r", with: "\n")
        var rows: [[String]] = []
        var field = ""
        var row: [String] = []
        var inQuotes = false
        var i = normalized.startIndex
        while i < normalized.endIndex {
            let c = normalized[i]
            if inQuotes {
                if c == "\"" {
                    let next = normalized.index(after: i)
                    if next < normalized.endIndex && normalized[next] == "\"" {
                        field.append("\"")
                        i = next
                    } else {
                        inQuotes = false
                    }
                } else {
                    field.append(c)
                }
            } else {
                switch c {
                case "\"":
                    inQuotes = true
                case ",":
                    row.append(field)
                    field = ""
                case "\n":
                    row.append(field)
                    rows.append(row)
                    row = []
                    field = ""
                default:
                    field.append(c)
                }
            }
            i = normalized.index(after: i)
        }
        // Last row (file may not end in newline)
        if !field.isEmpty || !row.isEmpty {
            row.append(field)
            rows.append(row)
        }
        return rows
    }
}

private extension String {
    func nonEmpty() -> String? { isEmpty ? nil : self }
}
