//===----------------------------------------------------------------------===//
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//

extension IPv6Address {
    /// Parses an IPv6 address string into an IPv6Address instance.
    ///
    /// Follows RFC 4291 and RFC 5952.
    ///
    /// This function supports standard IPv6 notation including:
    /// - Full addresses: `2001:0db8:0000:0042:0000:8a2e:0370:7334`
    /// - Zero compression: `2001:db8::8a2e:370:7334`
    /// - Leading zero omission: `2001:db8:0:42:0:8a2e:370:7334`
    /// - Unspecified address: `::`
    /// - Zone identifiers: `fe80::1%eth0`
    ///
    /// - Parameter input: IPv6 address string (with optional zone identifier after %)
    /// - Returns: An `IPv6Address` instance representing the parsed address
    ///
    /// ## Example Usage
    /// ```swift
    /// let addr1 = try IPv6Address.parse("2001:db8::1")
    /// let addr2 = try IPv6Address.parse("::") // Unspecified address
    /// let addr3 = try IPv6Address.parse("fe80::1%eth0") // With zone identifier
    /// ```
    static func parse(_ input: String) throws -> IPv6Address {
        var ipBytes = [UInt8](repeating: 0, count: 16)
        var ellipsisPosition: Int?

        // Extract zone identifier
        let (address, zone) = try extractZoneIdentifier(from: input)

        // RFC 4291 Section 2.2.3: IPv4 suffix must be at the end (last 32 bits)
        var remainingAddress = address
        var ipv6ByteLimit = 16  // Maximum bytes available for IPv6 hex groups
        var hasIPv4Suffix = false

        // check if the IPv6 address has IPv4 address in it.
        if let (ipv6Part, ipv4Bytes) = try extractIPv4Suffix(from: address) {
            // If IPv4 present, save directly to last 4 bytes.
            ipBytes[12] = ipv4Bytes[0]
            ipBytes[13] = ipv4Bytes[1]
            ipBytes[14] = ipv4Bytes[2]
            ipBytes[15] = ipv4Bytes[3]

            // Update address and limit IPv6 parsing to first 12 bytes (6 groups max)
            remainingAddress = ipv6Part
            ipv6ByteLimit = 12
            hasIPv4Suffix = true
        }

        // Handle leading ellipsis in the IPv6 part
        if remainingAddress.utf8.starts(with: [58, 58]) {  // "::"
            ellipsisPosition = 0
            remainingAddress = String(remainingAddress.dropFirst(2))

            // Special case: "::" represents the unspecified address (all zeros)
            // But if we have IPv4 suffix, the IPv4 bytes are already set correctly
            if remainingAddress.isEmpty {
                // If we have IPv4 suffix, ipBytes already has the IPv4 data, just return
                if hasIPv4Suffix {
                    return try Self(ipBytes, zone: zone)
                }

                // Pure "::" - Return the unspecified address, handling zone identifiers
                if let zone = zone, !zone.isEmpty {
                    return try Self(ipBytes, zone: zone)
                }
                return .unspecified
            }
        }

        // Parse IPv6 hex groups up to the byte limit
        var byteIndex = 0
        let utf8 = remainingAddress.utf8
        var currentPosition = utf8.startIndex

        while byteIndex < ipv6ByteLimit && currentPosition < utf8.endIndex {
            let (hexValue, nextPosition) = try parseHexadecimal(
                from: utf8,
                startingAt: currentPosition
            )

            // Store the UInt16 in network-byte order
            ipBytes[byteIndex] = UInt8(hexValue >> 8)
            ipBytes[byteIndex + 1] = UInt8(hexValue & 0xFF)
            byteIndex += 2
            currentPosition = nextPosition

            // Terminate early if we have consumed the whole string
            if currentPosition == utf8.endIndex {
                break
            }

            // Parse separator and handle ellipsis detection
            currentPosition = try skipColonSeparator(
                from: utf8,
                at: currentPosition,
                currentByteIndex: byteIndex,
                ellipsisPosition: &ellipsisPosition
            )
        }

        // Validate complete consumption of input
        guard currentPosition >= utf8.endIndex else {
            throw AddressError.malformedAddress
        }

        // Apply ellipsis expansion for the IPv6 portion
        try expandEllipsis(
            in: &ipBytes,
            parsedBytes: byteIndex,
            ellipsisPosition: ellipsisPosition,
            byteLimit: ipv6ByteLimit
        )
        let value = ipBytes.reduce(UInt128(0)) { ($0 << 8) | UInt128($1) }
        return Self(value, zone: zone)
    }

    // MARK: - Helper Functions

    /// Extracts IPv4 suffix if present at the end of the address
    ///
    /// Follows: RFC 4291 Section 2.2.3: Alternative form x:x:x:x:x:x:d.d.d.d
    /// IPv4 must be the last 32 bits and preceded by a colon
    ///
    /// - Parameter input: The IPv6 address string to check
    /// - Returns: Optional tuple of (IPv6 part without IPv4, IPv4 bytes array) if IPv4 found, nil otherwise
    /// - Throws: `AddressError.invalidIPv4Suffix` for invalid IPv4 addresses
    internal static func extractIPv4Suffix(from input: String) throws -> (String, [UInt8])? {
        // must contain a dot to be IPv4
        guard input.utf8.contains(46) else {  // ASCII '.'
            return nil
        }

        // IPv4 address must be present after last colon
        guard let lastColonIndex = input.lastIndex(of: ":") else {
            return nil
        }

        // TODO: maybe refactor for performance
        let afterColon = input.index(after: lastColonIndex)
        guard afterColon < input.endIndex else {
            return nil
        }

        let possibleIPv4 = String(input[afterColon...])
        guard let ipv4Value = try? IPv4Address.parse(possibleIPv4) else {
            throw AddressError.invalidIPv4SuffixInIPv6Address
        }

        // Check if lastColonIndex is the second ':' of '::'. If so, ensure to include it.
        let isDoubleColon = lastColonIndex > input.startIndex && input[input.index(before: lastColonIndex)] == ":"
        let ipv6Part = isDoubleColon ? String(input[...lastColonIndex]) : String(input[..<lastColonIndex])
        return (ipv6Part, IPv4Address.bytes(ipv4Value))
    }

    /// Extracts zone identifier
    ///
    /// - Parameter input: The full IPv6 address string with potential zone identifier
    /// - Returns: Tuple of (address part, optional zone identifier)
    /// - Throws: `AddressError.invalidZoneIdentifier` for malformed zone identifiers
    private static func extractZoneIdentifier(from input: String) throws -> (String, String?) {
        guard let percentIndex = input.lastIndex(of: "%") else {
            return (input, nil)
        }

        let zoneStartIndex = input.index(after: percentIndex)
        guard zoneStartIndex < input.endIndex else {
            throw AddressError.invalidZoneIdentifier
        }

        let addressPart = String(input[..<percentIndex])
        let zoneIdentifier = String(input[zoneStartIndex...])

        return (addressPart, zoneIdentifier)
    }

    /// Parses a hexadecimal group from an IPv6 address component.
    ///
    /// - Parameters:
    ///   - utf8: The UTF-8 view to parse from
    ///   - startIndex: Starting position in the UTF-8 view
    /// - Returns: Tuple of (parsed hex value as UInt16, next position after parsed digits)
    /// - Throws: `AddressError.invalidHexGroup` if no valid hex digits are found
    ///
    /// ## Example
    /// ```swift
    /// let utf8 = "2001:db8::1".utf8
    /// let (value, nextPos) = try parseHexadecimal(from: utf8, startingAt: utf8.startIndex)
    /// // value = 0x2001, nextPos points to ':'
    /// ```
    @inlinable
    internal static func parseHexadecimal(
        from group: String.UTF8View,
        startingAt startIndex: String.UTF8View.Index
    ) throws -> (UInt16, String.UTF8View.Index) {
        var accumulator: UInt16 = 0
        var digitCount = 0
        var currentIndex = startIndex

        while currentIndex < group.endIndex && digitCount < 4 {
            let byte = group[currentIndex]

            // Fast hex digit parsing using ASCII values
            let hexValue: UInt16
            if byte >= 48 && byte <= 57 {  // '0'-'9'
                hexValue = UInt16(byte - 48)
            } else if byte >= 65 && byte <= 70 {  // 'A'-'F'
                hexValue = UInt16(byte - 65 + 10)
            } else if byte >= 97 && byte <= 102 {  // 'a'-'f'
                hexValue = UInt16(byte - 97 + 10)
            } else {
                break  // Not a hex digit
            }

            accumulator = (accumulator << 4) + hexValue
            digitCount += 1
            currentIndex = group.index(after: currentIndex)
        }

        guard digitCount > 0 else {
            // No hex digits found
            throw AddressError.invalidHexGroup
        }
        return (accumulator, currentIndex)
    }

    /// Parses a colon separator between IPv6 groups and detects ellipsis notation (::).
    ///
    /// - Parameters:
    ///   - utf8: The UTF-8 view being parsed
    ///   - position: Current position in the UTF-8 view (must point to a colon)
    ///   - currentByteIndex: Current byte index in the IP array
    ///   - ellipsisPosition: Inout parameter tracking ellipsis position
    /// - Returns: Next position in the UTF-8 view after parsing separator
    ///
    /// ## Example
    /// ```swift
    /// let utf8 = "2001:db8::1".utf8
    /// var ellipsisPos: Int? = nil
    /// // After parsing "2001", position points to first ':'
    /// let nextPos = try skipColonSeparator(from: utf8, at: position,
    ///                                       currentByteIndex: 2,
    ///                                       ellipsisPosition: &ellipsisPos)
    /// // For single colon: nextPos points to 'd' in 'db8'
    /// // For double colon (::): ellipsisPos = 2, nextPos points to '1'
    /// ```
    private static func skipColonSeparator(
        from group: String.UTF8View,
        at position: String.UTF8View.Index,
        currentByteIndex: Int,
        ellipsisPosition: inout Int?
    ) throws -> String.UTF8View.Index {
        // Expect colon separator
        guard group[position] == 58 else {  // ASCII ':'
            throw AddressError.malformedAddress
        }

        let afterFirstColon = group.index(after: position)
        guard afterFirstColon < group.endIndex else {
            // Trailing colon not allowed
            throw AddressError.malformedAddress
        }

        // Check for double colon, return position after that
        if group[afterFirstColon] == 58 {  // ASCII ':'
            guard ellipsisPosition == nil else {
                // Multiple :: not allowed
                throw AddressError.multipleEllipsis
            }
            ellipsisPosition = currentByteIndex
            let afterSecondColon = group.index(after: afterFirstColon)
            return afterSecondColon
        }
        return afterFirstColon
    }

    /// Expands ellipsis for IPv6 addresses
    ///
    /// - Parameters:
    ///   - ipBytes: Inout array of IP bytes to modify
    ///   - parsedBytes: Number of bytes already parsed for IPv6 groups
    ///   - ellipsisPosition: Optional position where ellipsis was found
    ///   - byteLimit: Maximum bytes available for IPv6 (16 for pure IPv6, 12 if IPv4 suffix present)
    /// - Throws: `AddressError.incompleteAddress` for invalid address lengths
    private static func expandEllipsis(
        in ipBytes: inout [UInt8],
        parsedBytes: Int,
        ellipsisPosition: Int?,
        byteLimit: Int = 16
    ) throws {
        guard let ellipsisPosition = ellipsisPosition else {
            // No ellipsis - validate we have exactly filled the available bytes
            guard parsedBytes == byteLimit else {
                throw AddressError.incompleteAddress  // Incomplete address without ellipsis
            }
            return
        }

        // Calculate expansion within the byte limit
        let bytesToExpand = byteLimit - parsedBytes
        guard bytesToExpand > 0 else {
            throw AddressError.malformedAddress  // No room for ellipsis expansion
        }

        let suffixBytes = Array(ipBytes[ellipsisPosition..<parsedBytes])
        let targetStartIndex = byteLimit - suffixBytes.count

        // Clear the expansion area using Swift's range-based assignment
        for i in ellipsisPosition..<targetStartIndex {
            ipBytes[i] = 0
        }

        // Place suffix at the end of the IPv6 section using Swift's collection assignment
        for (offset, byte) in suffixBytes.enumerated() {
            ipBytes[targetStartIndex + offset] = byte
        }
    }

}
