use std::fs;
use std::process::exit;
use std::io::Read;

use anyhow::Result;

use clap::Parser;
use log::{info, debug, error};
use serde::Deserialize;
use rustyline_async::{Readline, ReadlineEvent};

use matrix_sdk::{matrix_auth::{MatrixSession, MatrixSessionTokens}, ruma::{api::client::{authenticated_media, media}, device_id, user_id}, Client, SessionMeta};
use matrix_sdk::ruma::events::room::{MediaSource, message::{MessageType, RoomMessageEventContent}};

// NOTE: This has been stolen from the matrix-rust-sdk to patch out checks for authenticated media
async fn get_media_content(
    client: &Client,
    source: &MediaSource,
    use_auth: bool
) -> matrix_sdk::Result<Vec<u8>> {
    let content: Vec<u8> = match &source {
        MediaSource::Encrypted(file) => {
            let content = if use_auth {
                let request = authenticated_media::get_content::v1::Request::from_uri(&file.url)?;
                client.send(request, None).await?.file
            } else {
                #[allow(deprecated)]
                let request = media::get_content::v3::Request::from_url(&file.url)?;
                client.send(request, None).await?.file
            };

            let content = {
                debug!("Decrypting file");

                let content_len = content.len();
                let mut cursor = std::io::Cursor::new(content);
                let mut reader = match matrix_sdk::crypto::AttachmentDecryptor::new(
                    &mut cursor,
                    file.as_ref().clone().into(),
                ) {
                    Ok(reader) => reader,
                    Err(error) => {
                        error!("Unable to decrypt: {}", error);
                        exit(1);
                    }
                };

                // Encrypted size should be the same as the decrypted size,
                // rounded up to a cipher block.
                let mut decrypted = Vec::with_capacity(content_len);

                reader.read_to_end(&mut decrypted)?;

                decrypted
            };

            content
        }
        MediaSource::Plain(uri) => {
            debug!("File doesn't appear to be encrypted");

            if use_auth {
                let request = authenticated_media::get_content::v1::Request::from_uri(uri)?;
                client.send(request, None).await?.file
            } else {
                #[allow(deprecated)]
                let request = media::get_content::v3::Request::from_url(uri)?;
                client.send(request, None).await?.file
            }
        }
    };

    Ok(content)
}

async fn process_media(source: MediaSource, body: Option<String>, server: Option<String>) -> Result<()> {
    let uri = match source.clone() {
        MediaSource::Plain(uri) => uri,
        MediaSource::Encrypted(encrypted) => encrypted.url,
    };

    let server_name = uri.server_name()?;
    let media_id = uri.media_id()?;

    let client = match server {
        Some(ref url) => Client::builder().server_name_or_homeserver_url(url),
        None => Client::builder().server_name(server_name),
    }.build().await?;

    debug!("Downloading {} from {}", uri, client.homeserver());

    let content = match get_media_content(&client, &source, false).await {
        Ok(content) => content,
        Err(error) => {
            let error_is_needs_authentication = match &error {
                matrix_sdk::Error::Http(http) => match http {
                    matrix_sdk::HttpError::IntoHttp(into_http) => {
                        match into_http {
                            matrix_sdk::ruma::api::error::IntoHttpError::NeedsAuthentication => true,
                            _ => false,
                        }
                    },
                    _ => false
                },
                _ => false,
            };

            if error_is_needs_authentication {
                info!("The server did not allow downloading without authentication, please enter an access token:");

                let token = rpassword::prompt_password("> ")?;

                client.restore_session(MatrixSession {
                    meta: SessionMeta {
                        user_id: user_id!("@unused:example.com").to_owned(),
                        device_id: device_id!("UNUSED").to_owned(),
                    },
                    tokens: MatrixSessionTokens {
                        access_token: token,
                        refresh_token: None,
                    },
                }).await?;

                get_media_content(&client, &source, false).await
                    .map_err(|error| anyhow::Error::msg(error.to_string()))?
            } else {
                return Err(anyhow::Error::msg(error.to_string()));
            }
        }
    };

    let file_name = match body {
        Some(body) => format!("{server_name}-{media_id}-{}", body),
        None => format!("{server_name}-{media_id}")
    };

    info!("Saving to {file_name}");

    fs::write(file_name, content)?;

    Ok(())
}

#[derive(Deserialize, Debug)]
struct DecryptedEventSource {
    content: RoomMessageEventContent,
}

#[derive(clap::Parser)]
#[command(author, version, about, long_about = None)]
struct Arguments {
    /// Homeserver URL or Name
    #[clap(long, env)]
    server: Option<String>,
}

#[tokio::main]
async fn main() -> Result<()> {
    env_logger::init_from_env(env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "matrix_media_event_decrypt=debug"));

    let Arguments {
        server
    } = Arguments::parse();

    info!("Enter decrypted event source, confirm with Ctrl+D");

    let (mut readline, mut _stdout) = Readline::new("> ".to_owned()).unwrap();
    let mut lines = Vec::new();

    loop {
        match readline.readline().await {
            Ok(ReadlineEvent::Line(line)) => {
                let line = line.trim();
                if line.is_empty() {
                    break;
                }

                // readline.add_history_entry(line.to_owned());
                lines.push(line.to_owned());
            },
            Ok(ReadlineEvent::Eof) => {
                break
            },
            Ok(ReadlineEvent::Interrupted) => {
                println!("^C");
                exit(1);
            },
            Err(error) => {
                error!("Unable to read in decrypted event source: {}", error.to_string());
                exit(1);
            },
        }
	}

    drop(_stdout);
    drop(readline);

    let event = match serde_json::from_str::<DecryptedEventSource>(lines.join("\n").as_str()) {
        Ok(event) => event,
        Err(error) => {
            error!("Unable to parse event: {}", error);
            exit(1);
        }
    };

    println!();

    debug!("Parsed event with {} content", event.content.msgtype.msgtype());

    if let Err(error) = match event.content.msgtype {
        MessageType::Audio(content) => process_media(content.source, Some(content.body), server).await,
        MessageType::File(content) => process_media(content.source, Some(content.body), server).await,
        MessageType::Image(content) => process_media(content.source, Some(content.body), server).await,
        MessageType::Video(content) => process_media(content.source, Some(content.body), server).await,
        _ => {
            error!("Event content is not known to contain media, exiting");
            exit(1);
        },
    } {
        error!("Unable to process media: {}", error);
    }

    Ok(())
}