#[derive(Debug, Clone)]
pub struct Url {
    pub scheme: String,
    pub host: String,
    pub port: u16,
    pub path: String,
    pub query: String,
    pub full: String,
}

impl Url {
    pub fn parse(input: &str) -> Result<Self, String> {
        let input = input.trim();

        let (scheme, rest) = match input.find("://") {
            Some(pos) => {
                let s = &input[..pos].to_lowercase();
                let rest = &input[pos + 3..];
                (s.to_string(), rest)
            }
            None => return Err(format!("missing scheme in url: {input}")),
        };

        if scheme != "http" && scheme != "https" && scheme != "ws" && scheme != "wss" {
            return Err(format!("unsupported scheme: {scheme}"));
        }

        let default_port: u16 = match scheme.as_str() {
            "https" | "wss" => 443,
            _ => 80,
        };
        let is_tls = scheme == "https" || scheme == "wss";

        let (host_str, path_and_query) = match rest.find('/') {
            Some(pos) => (&rest[..pos], &rest[pos..]),
            None => (rest, "/"),
        };

        let (host, port) = if let Some(colon_pos) = host_str.rfind(':') {
            let h = &host_str[..colon_pos];
            let p_str = &host_str[colon_pos + 1..];
            let p: u16 = p_str
                .parse()
                .map_err(|_| format!("invalid port in url: {input}"))?;
            (h.to_string(), p)
        } else {
            (host_str.to_string(), default_port)
        };

        if host.is_empty() {
            return Err(format!("empty host in url: {input}"));
        }

        let (path, query) = if let Some(qmark) = path_and_query.find('?') {
            let p = if qmark == 0 {
                ""
            } else {
                &path_and_query[..qmark]
            };
            let p = if p.is_empty() { "/" } else { p };
            (p.to_string(), path_and_query[qmark..].to_string())
        } else {
            let p = if path_and_query.is_empty() {
                "/"
            } else {
                path_and_query
            };
            (p.to_string(), String::new())
        };

        Ok(Url {
            scheme: if is_tls {
                "https".into()
            } else {
                "http".into()
            },
            host,
            port,
            path,
            query,
            full: input.to_string(),
        })
    }

    pub fn origin(&self) -> String {
        if self.port == 80 || self.port == 443 {
            format!("{}://{}", self.scheme, self.host)
        } else {
            format!("{}://{}:{}", self.scheme, self.host, self.port)
        }
    }

    pub fn is_tls(&self) -> bool {
        self.port == 443 || self.scheme == "https"
    }

    pub fn request_target(&self) -> String {
        if self.query.is_empty() {
            self.path.clone()
        } else {
            format!("{}?{}", self.path, self.query)
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_https() {
        let u = Url::parse("https://example.com/path?a=1").unwrap();
        assert_eq!(u.scheme, "https");
        assert_eq!(u.host, "example.com");
        assert_eq!(u.port, 443);
        assert_eq!(u.path, "/path");
        assert_eq!(u.query, "?a=1");
        assert!(u.is_tls());
    }

    #[test]
    fn test_parse_http_with_port() {
        let u = Url::parse("http://localhost:8080/api").unwrap();
        assert_eq!(u.scheme, "http");
        assert_eq!(u.host, "localhost");
        assert_eq!(u.port, 8080);
        assert_eq!(u.path, "/api");
        assert!(!u.is_tls());
    }

    #[test]
    fn test_parse_ws() {
        let u = Url::parse("ws://echo.example.com/chat").unwrap();
        assert_eq!(u.host, "echo.example.com");
        assert_eq!(u.port, 80);
    }

    #[test]
    fn test_parse_root() {
        let u = Url::parse("http://example.com").unwrap();
        assert_eq!(u.path, "/");
        assert_eq!(u.query, "");
    }

    #[test]
    fn test_parse_no_scheme() {
        assert!(Url::parse("example.com/path").is_err());
    }

    #[test]
    fn test_parse_bad_port() {
        assert!(Url::parse("http://example.com:abc/path").is_err());
    }

    #[test]
    fn test_parse_empty_host() {
        assert!(Url::parse("http:///path").is_err());
    }
}
