//! Simple file-reader TCP server with framed stream.
//!
//! Using the following command:
//!
//! ```sh
//! nc 127.0.0.1 8080
//! ```
//!
//! Follow the prompt and enter a file path, relative or absolute.

use std::io;

use actix_codec::{Framed, LinesCodec};
use actix_rt::net::TcpStream;
use actix_server::Server;
use actix_service::{fn_service, ServiceFactoryExt as _};
use futures_util::{SinkExt as _, StreamExt as _};
use tokio::{fs::File, io::AsyncReadExt as _};

async fn run() -> io::Result<()> {
    env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));

    let addr = ("127.0.0.1", 8080);
    tracing::info!("starting server on port: {}", &addr.0);

    // Bind socket address and start worker(s). By default, the server uses the number of physical
    // CPU cores as the worker count. For this reason, the closure passed to bind needs to return
    // a service *factory*; so it can be created once per worker.
    Server::build()
        .bind("file-reader", addr, move || {
            fn_service(move |stream: TcpStream| async move {
                // set up codec to use with I/O resource
                let mut framed = Framed::new(stream, LinesCodec::default());

                loop {
                    // prompt for file name
                    framed.send("Type file name to return:").await?;

                    // wait for next line
                    match framed.next().await {
                        Some(Ok(line)) => {
                            match File::open(&line).await {
                                Ok(mut file) => {
                                    tracing::info!("reading file: {}", &line);

                                    // read file into String buffer
                                    let mut buf = String::new();
                                    file.read_to_string(&mut buf).await?;

                                    // send String into framed object
                                    framed.send(buf).await?;

                                    // break out of loop and
                                    break;
                                }
                                Err(err) => {
                                    tracing::error!("{}", err);
                                    framed
                                        .send("File not found or not readable. Try again.")
                                        .await?;
                                    continue;
                                }
                            };
                        }

                        // not being able to read a line from the stream is unrecoverable
                        Some(Err(err)) => return Err(err),

                        // This EOF won't be hit.
                        None => continue,
                    }
                }

                // close connection after file has been copied to TCP stream
                Ok(())
            })
            .map_err(|err| tracing::error!("service error: {:?}", err))
        })?
        .workers(2)
        .run()
        .await
}

#[tokio::main]
async fn main() -> io::Result<()> {
    run().await?;
    Ok(())
}

// alternatively:
// #[actix_rt::main]
// async fn main() -> io::Result<()> {
//     run().await?;
//     Ok(())
// }