import Foundation

/// A paired web-terminal host (frozen contract, plan §3.1). The SINGLE point of
/// derivation for the `Origin` header and the WS URL — hand-assembling either
/// anywhere else is a review CRITICAL (plan §5.1 / T-iOS-9 安全注).
///
/// Derivations are computed once at init and stored immutably:
/// - `originHeader` = `<scheme>://<host>[:<port>]`, omitting the scheme's
///   default port (http/80, https/443), scheme+host lowercased — identical to
///   browser Origin serialization. The server normalises BOTH sides via
///   `new URL()` before comparing protocol/hostname/port (src/http/origin.ts:31-51).
/// - `wsURL` = same host+port, scheme http→ws / https→wss, path replaced by
///   `WireConstants.wsPath`; query/fragment/credentials dropped.
public struct HostEndpoint: Sendable, Equatable, Codable {
    /// The URL the user dialed: `http(s)://<host>[:<port>]`. Any path, query,
    /// fragment or credentials it carries are ignored by the derivations.
    public let baseURL: URL
    /// Derived WS endpoint (`ws(s)://…/term`). Stored at init; contract-wise a
    /// read-only property, per plan §3.1.
    public let wsURL: URL
    /// Derived `Origin` header value — see type doc. Never hand-assemble.
    public let originHeader: String

    private static let wsSchemeByHTTPScheme = ["http": "ws", "https": "wss"]
    private static let defaultPortByScheme = ["http": 80, "https": 443]

    /// Validating init: the URL must be http(s) with a non-empty host, else nil
    /// (QR-scan payloads are untrusted external input — reject early, plan §5).
    public init?(baseURL: URL) {
        guard let components = URLComponents(url: baseURL, resolvingAgainstBaseURL: true),
              let scheme = components.scheme?.lowercased(),
              let wsScheme = Self.wsSchemeByHTTPScheme[scheme],
              let rawHost = components.host, !rawHost.isEmpty,
              let wsURL = Self.deriveWSURL(from: components, wsScheme: wsScheme)
        else { return nil }

        self.baseURL = baseURL
        self.wsURL = wsURL
        self.originHeader = Self.deriveOrigin(
            scheme: scheme, host: rawHost.lowercased(), port: components.port
        )
    }

    private static func deriveOrigin(scheme: String, host: String, port: Int?) -> String {
        // IPv6 literals need brackets in an Origin. URLComponents.host has
        // returned them both bare and pre-bracketed across Foundation versions —
        // wrap idempotently.
        let needsBrackets = host.contains(":") && !host.hasPrefix("[")
        let serializedHost = needsBrackets ? "[\(host)]" : host
        guard let port, port != defaultPortByScheme[scheme] else {
            return "\(scheme)://\(serializedHost)"
        }
        return "\(scheme)://\(serializedHost):\(port)"
    }

    private static func deriveWSURL(from components: URLComponents, wsScheme: String) -> URL? {
        var wsComponents = components
        wsComponents.scheme = wsScheme
        wsComponents.path = WireConstants.wsPath
        wsComponents.query = nil
        wsComponents.fragment = nil
        wsComponents.user = nil
        wsComponents.password = nil
        return wsComponents.url
    }

    // MARK: - Codable (persisted via HostRegistry/Keychain — re-validate on decode)

    private enum CodingKeys: String, CodingKey {
        case baseURL
    }

    public init(from decoder: any Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let url = try container.decode(URL.self, forKey: .baseURL)
        guard let endpoint = HostEndpoint(baseURL: url) else {
            throw DecodingError.dataCorruptedError(
                forKey: .baseURL, in: container,
                debugDescription: "baseURL is not an http(s) URL with a host"
            )
        }
        self = endpoint
    }

    public func encode(to encoder: any Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(baseURL, forKey: .baseURL)
    }
}
