projects involving the gemini protocol
use std::io::{stdin, Read, Write};

use anyhow::{anyhow, Result};
use libtls::{config::Builder, tls::Tls};
use pico_args::Arguments;
use url::{Position, Url};

#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
enum Status {
    Input,
    Success,
    Redirect,
    TemporaryFailure,
    PermanentFailure,
    ClientCertificateRequired,
}

#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
struct Header {
    status: u8,
    meta: String,
}

impl Header {
    pub fn status(&self) -> Status {
        if self.status >= 60 {
            Status::ClientCertificateRequired
        } else if self.status >= 50 {
            Status::PermanentFailure
        } else if self.status >= 40 {
            Status::TemporaryFailure
        } else if self.status >= 30 {
            Status::Redirect
        } else if self.status >= 20 {
            Status::Success
        } else if self.status >= 10 {
            Status::Input
        } else {
            panic!("invalid status: {}", self.status)
        }
    }
}

fn parse_address(mut address: String) -> Result<Url> {
    if !address.trim_start().starts_with("gemini://") {
        if address.contains("://") {
            return Err(anyhow!("only gemini is supported"));
        }

        address = format!("gemini://{}", address);
    }

    let mut url = Url::parse(address.as_str())?;

    if !url.username().is_empty() || url.password().is_some() {
        return Err(anyhow!("no user info is allowed"));
    }

    if !url.has_host() {
        return Err(anyhow!("must supply host"));
    }

    if url.port().is_none() {
        url.set_port(Some(1965)).expect("port error")
    }

    Ok(url)
}

fn make_request(url: &Url) -> Result<(Header, String)> {
    let mut client = Tls::client()?;
    let config = Builder::new().noverifycert().build()?;
    client.configure(&config)?;

    // Open Connection
    client.connect(&url[Position::BeforeHost..Position::AfterPort], None)?;

    // Complete TLS handshake
    let shaken = client.tls_handshake()?;

    if shaken != 0 {
        return Err(anyhow!("tls handshake failed"));
    }

    // Validate Server Certificate
    // per section 4.2, we opt not to validate TLS connections beyond the handshake

    // Send request
    let req = format!("{}\r\n", url);
    let written = client.write(req.as_bytes())?;

    if written == 0 {
        return Err(anyhow!("failed to write request"));
    } else if written != req.len() {
        eprintln!(
            "warning: request was {} bytes, only wrote {}",
            req.len(),
            written
        )
    }

    // Handle response
    let mut res = String::new();
    let read = client.read_to_string(&mut res)?;

    if read == 0 {
        return Err(anyhow!("failed to read response"));
    }

    let line_break = res
        .match_indices("\r\n")
        .next()
        .expect("response missing CRLF")
        .0;

    // 2 status + 1 space + 1024 meta
    if line_break >= 1028 {
        return Err(anyhow!("meta was too long"));
    }

    let body = res.split_off(line_break);
    let header = Header {
        status: (&res[..2]).parse().unwrap(),
        meta: (&res[2..]).to_string(),
    };

    Ok((header, body))
}

fn handle_response(url: Url, header: Header, body: String) -> Result<()> {
    println!("status: {}", header.status);
    match header.status() {
        Status::Input => {
            println!("server is requesting input");
            println!("{}", header.meta);
            print!("> ");

            let mut line = String::from("?");
            stdin().read_line(&mut line)?;

            let with_input = url.join(line.as_str())?;

            println!("connecting to {} again with new input", with_input);

            let (header, body) = make_request(&with_input)?;

            handle_response(with_input, header, body)?
        }
        Status::Success => {
            // ignoring meta
            println!("{}", body)
        }
        Status::Redirect => {
            let address = header.meta;
            let redirect = parse_address(address)?;

            println!("redirecting {} to {}", url, redirect);

            let (header, body) = make_request(&redirect)?;

            handle_response(redirect, header, body)?
        }
        Status::TemporaryFailure | Status::PermanentFailure => {
            eprintln!("error from server");
            eprintln!("{}", header.meta)
        }
        Status::ClientCertificateRequired => {
            eprintln!("server requires a client certificate");
            eprintln!("{}", header.meta)
        }
    };
    Ok(())
}

fn main() -> Result<()> {
    let mut args = Arguments::from_env();
    let address: String = args.free_from_str()?.expect("must supply address");
    let url = parse_address(address)?;

    println!("connecting to {}", url);

    let (header, body) = make_request(&url)?;

    handle_response(url, header, body)
}