Toy implementation of the gemini protocol
use crate::network::Network;
use crate::utils::{BufReadExt, ReadExt};
use crate::{response, Ui};
use bstr::ByteVec;
use std::io::{BufReader, Read, Result as ioResult};
use url::Url;

pub fn run<N: Network>(net: &mut N, ui: &mut impl Ui, mut url: Url)
where
    <N as Network>::Error: std::fmt::Debug,
{
    while let Some(new_url) = run_url(net, ui, url) {
        url = new_url;
    }
}

pub(crate) fn run_url<N: Network>(net: &mut N, ui: &mut impl Ui, url: Url) -> Option<Url>
where
    <N as Network>::Error: std::fmt::Debug,
{
    // must make a new connection per request
    let mut stream = net.connect(&url).expect("Failed to connect");

    send_request(&mut stream, &url).expect("request failed to send");

    let head = read_response_header(ui, &mut stream).expect("failed to read header");
    // println!("head: {head:?}");

    handle_response_header(ui, head, url, stream)
}

fn handle_response_header(
    ui: &mut impl Ui,
    head: response::Header,
    url: Url,
    stream: impl Read,
) -> Option<Url> {
    match head {
        response::Header::Input(s) => handle::input(s, ui, url),
        response::Header::Success(s) => {
            handle::success(s, stream, ui);
            None
        }
        response::Header::Redirect(s) => Some(handle::redirect(s, ui)),
        response::Header::FailTemp(s) => {
            handle::fail_temp(s, ui);
            None
        }
        response::Header::FailPerm(s) => {
            handle::fail_perm(s, ui);
            None
        }
        response::Header::CertRequired(s) => {
            handle::cert_required(s, ui);
            None
        }
    }
}

pub mod handle {
    use crate::{
        decode_media,
        response::header::{CertRequired, FailPerm, FailTemp, Input, Redirect, Success},
        utils::ReadExt,
        Ui,
    };
    use std::io::Read;
    use url::Url;

    pub fn cert_required(s: CertRequired, ui: &mut impl Ui) {
        let CertRequired { message, typ } = s;
        ui.warn(format_args!(
            "Certificate required: {typ:?}! Details:\n{message}"
        ));
    }

    pub fn fail_perm(s: FailPerm, ui: &mut impl Ui) {
        let FailPerm { message, typ } = s;
        ui.warn(format_args!(
            "Permanent failure: {typ:?}! Details:\n{message}"
        ));
    }

    pub fn fail_temp(s: FailTemp, ui: &mut impl Ui) {
        let FailTemp { message, typ } = s;
        ui.warn(format_args!(
            "Temporary failure: {typ:?}! Please try again. Details:\n{message}"
        ));
    }

    pub fn redirect(s: Redirect, ui: &mut impl Ui) -> Url {
        let Redirect { url, temporary } = s;
        if let Some(false) = temporary {
            ui.warn(format_args!(
                "Server has permanently moved. Redirecting to: {url}\nPlease update your records!"
            ));
        } else {
            ui.warn(format_args!(
                "Server has temporarily moved. Redirecting to: {url}"
            ));
        }
        url
    }

    pub fn success(s: Success, mut stream: impl Read, ui: &mut impl Ui) {
        let Success { mime } = s;
        let body = stream.read_string().expect("failed to read body");
        //TODO remove this debug output
        // println!("page is encoded as {mime:?}");
        // println!("response body: {body}");
        let media = decode_media(mime, &body);
        ui.show(media);
    }

    pub fn input(s: Input, ui: &mut impl Ui, mut url: Url) -> Option<Url> {
        let Input { prompt, sensitive } = s;
        let input = if let Some(true) = sensitive {
            ui.read_secret(&prompt)
        } else {
            ui.read(&prompt)
        }
        .expect("failed to read input from ui");
        let input = urlencoding::encode(&input);
        url.set_query(Some(&input));
        Some(url)
    }
}

pub fn read_response_header(ui: &mut impl Ui, mut stream: impl Read) -> ioResult<response::Header> {
    let status = stream.read_n::<2>()?;

    let c = stream.read_byte()?;
    if c != b' ' {
        ui.warn(format_args!(
            "illegal separator byte '0x{c:02X}' was not a space"
        ));
    }

    // don't read 1024, but rather read until CRLF
    // TODO limit to 1024
    let mut meta = Vec::new();
    BufReader::new(stream).read_until(b"\r\n", &mut meta)?;

    // println!(
    //     "status line: \"{}{}{}\"",
    //     std::str::from_utf8(&status).unwrap(),
    //     std::str::from_utf8(&[c]).unwrap(),
    //     std::str::from_utf8(&*meta).unwrap()
    // );

    // turn status from ascii to numeric
    let status = status.map(|s| s.checked_sub(b'0').expect("bad status fragment"));
    // drop last 2
    for _ in 0..2 {
        meta.pop();
    }

    let meta = response::Meta(meta.into_string().expect("Meta was not valid UTF8"));
    let status = response::raw::Header { meta, status }
        .try_into()
        .expect("failed to parse header");
    Ok(status)
}

pub fn send_request(stream: &mut impl std::io::Write, url: &Url) -> ioResult<usize> {
    let mut sent = 0;
    sent += stream.write(url.as_str().as_bytes())?;
    sent += stream.write("\r\n".as_bytes())?;
    Ok(sent)
}

#[cfg(test)]
mod tests {
    use rstest::*;
    use url::Url;

    use super::{read_response_header, send_request};
    use crate::response::{header, Header};

    #[rstest]
    fn test_handle_redirect(
        #[values("http://url.com")] expect_url: Url,
        #[values(None, Some(true), Some(false))] temporary: Option<bool>,
    ) {
        let head = Header::Redirect(header::Redirect {
            url: expect_url.clone(),
            temporary,
        });
        let mut app = crate::cli::Cli {};
        let url = {
            let Header::Redirect(s) = head
            else {
                panic!("expected redirect")
            };
            crate::core::handle::redirect(s, &mut app)
        };
        assert_eq!(url, expect_url);
    }

    #[rstest]
    #[case('0', Some(false))]
    #[case('1', Some(true))]
    #[case('3', None)]
    fn test_response_input(#[case] sensitive_code: char, #[case] sensitive: Option<bool>) {
        let exp_head = Header::Input(header::Input {
            prompt: "prompt text".to_string(),
            sensitive,
        });

        let resp_text = format!("1{sensitive_code} prompt text\r\n");
        let resp_head: &[u8] = resp_text.as_bytes();
        let head = read_response_header(&mut crate::NopUi, &mut &*resp_head)
            .expect("failed to read response");
        assert_eq!(exp_head, head);
    }

    #[test]
    fn test_request() {
        let url: Url = "gemini://some/url".try_into().unwrap();
        let req = b"gemini://some/url\r\n";
        let mut net_stream = [0; 1024];

        send_request(&mut net_stream.as_mut_slice(), &url).expect("error sending request");

        assert_eq!(req, &net_stream[..req.len()]);
    }
}