use std::{collections::BTreeMap, convert::TryInto};

use super::{State, DEVICE_ID_LENGTH, SESSION_ID_LENGTH, TOKEN_LENGTH};
use crate::{pdu::PduBuilder, utils, ConduitResult, Database, Error, Ruma};
use ruma::{
    api::client::{
        error::ErrorKind,
        r0::{
            account::{
                change_password, deactivate, get_username_availability, register, whoami,
                ThirdPartyIdRemovalStatus,
            },
            uiaa::{AuthFlow, UiaaInfo},
        },
    },
    events::{
        room::canonical_alias, room::guest_access, room::history_visibility, room::join_rules,
        room::member, room::name, room::topic, EventType,
    },
    RoomAliasId, RoomId, RoomVersionId, UserId,
};

use register::RegistrationKind;
#[cfg(feature = "conduit_bin")]
use rocket::{get, post};

const GUEST_NAME_LENGTH: usize = 10;

/// # `GET /_matrix/client/r0/register/available`
///
/// Checks if a username is valid and available on this server.
///
/// - Returns true if no user or appservice on this server claimed this username
/// - This will not reserve the username, so the username might become invalid when trying to register
#[cfg_attr(
    feature = "conduit_bin",
    get("/_matrix/client/r0/register/available", data = "<body>")
)]
pub fn get_register_available_route(
    db: State<'_, Database>,
    body: Ruma<get_username_availability::Request<'_>>,
) -> ConduitResult<get_username_availability::Response> {
    // Validate user id
    let user_id = UserId::parse_with_server_name(body.username.clone(), db.globals.server_name())
        .ok()
        .filter(|user_id| {
            !user_id.is_historical() && user_id.server_name() == db.globals.server_name()
        })
        .ok_or(Error::BadRequest(
            ErrorKind::InvalidUsername,
            "Username is invalid.",
        ))?;

    // Check if username is creative enough
    if db.users.exists(&user_id)? {
        return Err(Error::BadRequest(
            ErrorKind::UserInUse,
            "Desired user ID is already taken.",
        ));
    }

    // TODO add check for appservice namespaces

    // If no if check is true we have an username that's available to be used.
    Ok(get_username_availability::Response { available: true }.into())
}

/// # `POST /_matrix/client/r0/register`
///
/// Register an account on this homeserver.
///
/// - Returns the device id and access_token unless `inhibit_login` is true
/// - When registering a guest account, all parameters except initial_device_display_name will be
/// ignored
/// - Creates a new account and a device for it
/// - The account will be populated with default account data
#[cfg_attr(
    feature = "conduit_bin",
    post("/_matrix/client/r0/register", data = "<body>")
)]
pub async fn register_route(
    db: State<'_, Database>,
    body: Ruma<register::Request<'_>>,
) -> ConduitResult<register::Response> {
    if db.globals.registration_disabled() {
        return Err(Error::BadRequest(
            ErrorKind::Forbidden,
            "Registration has been disabled.",
        ));
    }

    let is_guest = body.kind == RegistrationKind::Guest;

    let mut missing_username = false;

    // Validate user id
    let user_id = UserId::parse_with_server_name(
        if is_guest {
            utils::random_string(GUEST_NAME_LENGTH)
        } else {
            body.username.clone().unwrap_or_else(|| {
                // If the user didn't send a username field, that means the client is just trying
                // the get an UIAA error to see available flows
                missing_username = true;
                // Just give the user a random name. He won't be able to register with it anyway.
                utils::random_string(GUEST_NAME_LENGTH)
            })
        }
        .to_lowercase(),
        db.globals.server_name(),
    )
    .ok()
    .filter(|user_id| !user_id.is_historical() && user_id.server_name() == db.globals.server_name())
    .ok_or(Error::BadRequest(
        ErrorKind::InvalidUsername,
        "Username is invalid.",
    ))?;

    // Check if username is creative enough
    if !missing_username && db.users.exists(&user_id)? {
        return Err(Error::BadRequest(
            ErrorKind::UserInUse,
            "Desired user ID is already taken.",
        ));
    }

    // UIAA
    let mut uiaainfo = UiaaInfo {
        flows: vec![AuthFlow {
            stages: vec!["m.login.dummy".to_owned()],
        }],
        completed: Vec::new(),
        params: Default::default(),
        session: None,
        auth_error: None,
    };

    if let Some(auth) = &body.auth {
        let (worked, uiaainfo) =
            db.uiaa
                .try_auth(&user_id, "".into(), auth, &uiaainfo, &db.users, &db.globals)?;
        if !worked {
            return Err(Error::Uiaa(uiaainfo));
        }
    // Success!
    } else {
        uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
        db.uiaa.create(&user_id, "".into(), &uiaainfo)?;
        return Err(Error::Uiaa(uiaainfo));
    }

    if missing_username {
        return Err(Error::BadRequest(
            ErrorKind::MissingParam,
            "Missing username field.",
        ));
    }

    let password = if is_guest {
        None
    } else {
        body.password.clone()
    }
    .unwrap_or_default();

    // Create user
    db.users.create(&user_id, &password)?;

    // Initial data
    db.account_data.update(
        None,
        &user_id,
        EventType::PushRules,
        &ruma::events::push_rules::PushRulesEvent {
            content: ruma::events::push_rules::PushRulesEventContent {
                global: crate::push_rules::default_pushrules(&user_id),
            },
        },
        &db.globals,
    )?;

    if !is_guest && body.inhibit_login {
        return Ok(register::Response {
            access_token: None,
            user_id,
            device_id: None,
        }
        .into());
    }

    // Generate new device id if the user didn't specify one
    let device_id = if is_guest {
        None
    } else {
        body.device_id.clone()
    }
    .unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into());

    // Generate new token for the device
    let token = utils::random_string(TOKEN_LENGTH);

    // Add device
    db.users.create_device(
        &user_id,
        &device_id,
        &token,
        body.initial_device_display_name.clone(),
    )?;

    // If this is the first user on this server, create the admins room
    if db.users.count() == 1 {
        // Create a user for the server
        let conduit_user = UserId::parse_with_server_name("conduit", db.globals.server_name())
            .expect("@conduit:server_name is valid");

        db.users.create(&conduit_user, "")?;

        let room_id = RoomId::new(db.globals.server_name());

        let mut content = ruma::events::room::create::CreateEventContent::new(conduit_user.clone());
        content.federate = true;
        content.predecessor = None;
        content.room_version = RoomVersionId::Version6;

        // 1. The room create event
        db.rooms.build_and_append_pdu(
            PduBuilder {
                event_type: EventType::RoomCreate,
                content: serde_json::to_value(content).expect("event is valid, we just created it"),
                unsigned: None,
                state_key: Some("".to_owned()),
                redacts: None,
            },
            &conduit_user,
            &room_id,
            &db.globals,
            &db.sending,
            &db.account_data,
        )?;

        // 2. Make conduit bot join
        db.rooms.build_and_append_pdu(
            PduBuilder {
                event_type: EventType::RoomMember,
                content: serde_json::to_value(member::MemberEventContent {
                    membership: member::MembershipState::Join,
                    displayname: None,
                    avatar_url: None,
                    is_direct: None,
                    third_party_invite: None,
                })
                .expect("event is valid, we just created it"),
                unsigned: None,
                state_key: Some(conduit_user.to_string()),
                redacts: None,
            },
            &conduit_user,
            &room_id,
            &db.globals,
            &db.sending,
            &db.account_data,
        )?;

        // 3. Power levels
        let mut users = BTreeMap::new();
        users.insert(conduit_user.clone(), 100.into());
        users.insert(user_id.clone(), 100.into());

        db.rooms.build_and_append_pdu(
            PduBuilder {
                event_type: EventType::RoomPowerLevels,
                content: serde_json::to_value(
                    ruma::events::room::power_levels::PowerLevelsEventContent {
                        ban: 50.into(),
                        events: BTreeMap::new(),
                        events_default: 0.into(),
                        invite: 50.into(),
                        kick: 50.into(),
                        redact: 50.into(),
                        state_default: 50.into(),
                        users,
                        users_default: 0.into(),
                        notifications: ruma::events::room::power_levels::NotificationPowerLevels {
                            room: 50.into(),
                        },
                    },
                )
                .expect("event is valid, we just created it"),
                unsigned: None,
                state_key: Some("".to_owned()),
                redacts: None,
            },
            &conduit_user,
            &room_id,
            &db.globals,
            &db.sending,
            &db.account_data,
        )?;

        // 4.1 Join Rules
        db.rooms.build_and_append_pdu(
            PduBuilder {
                event_type: EventType::RoomJoinRules,
                content: serde_json::to_value(join_rules::JoinRulesEventContent::new(
                    join_rules::JoinRule::Invite,
                ))
                .expect("event is valid, we just created it"),
                unsigned: None,
                state_key: Some("".to_owned()),
                redacts: None,
            },
            &conduit_user,
            &room_id,
            &db.globals,
            &db.sending,
            &db.account_data,
        )?;

        // 4.2 History Visibility
        db.rooms.build_and_append_pdu(
            PduBuilder {
                event_type: EventType::RoomHistoryVisibility,
                content: serde_json::to_value(
                    history_visibility::HistoryVisibilityEventContent::new(
                        history_visibility::HistoryVisibility::Shared,
                    ),
                )
                .expect("event is valid, we just created it"),
                unsigned: None,
                state_key: Some("".to_owned()),
                redacts: None,
            },
            &conduit_user,
            &room_id,
            &db.globals,
            &db.sending,
            &db.account_data,
        )?;

        // 4.3 Guest Access
        db.rooms.build_and_append_pdu(
            PduBuilder {
                event_type: EventType::RoomGuestAccess,
                content: serde_json::to_value(guest_access::GuestAccessEventContent::new(
                    guest_access::GuestAccess::Forbidden,
                ))
                .expect("event is valid, we just created it"),
                unsigned: None,
                state_key: Some("".to_owned()),
                redacts: None,
            },
            &conduit_user,
            &room_id,
            &db.globals,
            &db.sending,
            &db.account_data,
        )?;

        // 6. Events implied by name and topic
        db.rooms.build_and_append_pdu(
            PduBuilder {
                event_type: EventType::RoomName,
                content: serde_json::to_value(
                    name::NameEventContent::new("Admin Room".to_owned()).map_err(|_| {
                        Error::BadRequest(ErrorKind::InvalidParam, "Name is invalid.")
                    })?,
                )
                .expect("event is valid, we just created it"),
                unsigned: None,
                state_key: Some("".to_owned()),
                redacts: None,
            },
            &conduit_user,
            &room_id,
            &db.globals,
            &db.sending,
            &db.account_data,
        )?;

        db.rooms.build_and_append_pdu(
            PduBuilder {
                event_type: EventType::RoomTopic,
                content: serde_json::to_value(topic::TopicEventContent {
                    topic: format!("Manage {}", db.globals.server_name()),
                })
                .expect("event is valid, we just created it"),
                unsigned: None,
                state_key: Some("".to_owned()),
                redacts: None,
            },
            &conduit_user,
            &room_id,
            &db.globals,
            &db.sending,
            &db.account_data,
        )?;

        // Room alias
        let alias: RoomAliasId = format!("#admins:{}", db.globals.server_name())
            .try_into()
            .expect("#admins:server_name is a valid alias name");

        db.rooms.build_and_append_pdu(
            PduBuilder {
                event_type: EventType::RoomCanonicalAlias,
                content: serde_json::to_value(canonical_alias::CanonicalAliasEventContent {
                    alias: Some(alias.clone()),
                    alt_aliases: Vec::new(),
                })
                .expect("event is valid, we just created it"),
                unsigned: None,
                state_key: Some("".to_owned()),
                redacts: None,
            },
            &conduit_user,
            &room_id,
            &db.globals,
            &db.sending,
            &db.account_data,
        )?;

        db.rooms.set_alias(&alias, Some(&room_id), &db.globals)?;

        // Invite and join the real user
        db.rooms.build_and_append_pdu(
            PduBuilder {
                event_type: EventType::RoomMember,
                content: serde_json::to_value(member::MemberEventContent {
                    membership: member::MembershipState::Invite,
                    displayname: None,
                    avatar_url: None,
                    is_direct: None,
                    third_party_invite: None,
                })
                .expect("event is valid, we just created it"),
                unsigned: None,
                state_key: Some(user_id.to_string()),
                redacts: None,
            },
            &conduit_user,
            &room_id,
            &db.globals,
            &db.sending,
            &db.account_data,
        )?;
        db.rooms.build_and_append_pdu(
            PduBuilder {
                event_type: EventType::RoomMember,
                content: serde_json::to_value(member::MemberEventContent {
                    membership: member::MembershipState::Join,
                    displayname: None,
                    avatar_url: None,
                    is_direct: None,
                    third_party_invite: None,
                })
                .expect("event is valid, we just created it"),
                unsigned: None,
                state_key: Some(user_id.to_string()),
                redacts: None,
            },
            &user_id,
            &room_id,
            &db.globals,
            &db.sending,
            &db.account_data,
        )?;
    }

    Ok(register::Response {
        access_token: Some(token),
        user_id,
        device_id: Some(device_id),
    }
    .into())
}

/// # `POST /_matrix/client/r0/account/password`
///
/// Changes the password of this account.
///
/// - Invalidates all other access tokens if logout_devices is true
/// - Deletes all other devices and most of their data (to-device events, last seen, etc.) if
/// logout_devices is true
#[cfg_attr(
    feature = "conduit_bin",
    post("/_matrix/client/r0/account/password", data = "<body>")
)]
pub fn change_password_route(
    db: State<'_, Database>,
    body: Ruma<change_password::Request<'_>>,
) -> ConduitResult<change_password::Response> {
    let sender_id = body.sender_id.as_ref().expect("user is authenticated");
    let device_id = body.device_id.as_ref().expect("user is authenticated");

    let mut uiaainfo = UiaaInfo {
        flows: vec![AuthFlow {
            stages: vec!["m.login.password".to_owned()],
        }],
        completed: Vec::new(),
        params: Default::default(),
        session: None,
        auth_error: None,
    };

    if let Some(auth) = &body.auth {
        let (worked, uiaainfo) = db.uiaa.try_auth(
            &sender_id,
            device_id,
            auth,
            &uiaainfo,
            &db.users,
            &db.globals,
        )?;
        if !worked {
            return Err(Error::Uiaa(uiaainfo));
        }
    // Success!
    } else {
        uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
        db.uiaa.create(&sender_id, &device_id, &uiaainfo)?;
        return Err(Error::Uiaa(uiaainfo));
    }

    db.users.set_password(&sender_id, &body.new_password)?;

    // TODO: Read logout_devices field when it's available and respect that, currently not supported in Ruma
    // See: https://github.com/ruma/ruma/issues/107
    // Logout all devices except the current one
    for id in db
        .users
        .all_device_ids(&sender_id)
        .filter_map(|id| id.ok())
        .filter(|id| id != device_id)
    {
        db.users.remove_device(&sender_id, &id)?;
    }

    Ok(change_password::Response.into())
}

/// # `GET _matrix/client/r0/account/whoami`
///
/// Get user_id of this account.
///
/// - Also works for Application Services
#[cfg_attr(
    feature = "conduit_bin",
    get("/_matrix/client/r0/account/whoami", data = "<body>")
)]
pub fn whoami_route(body: Ruma<whoami::Request>) -> ConduitResult<whoami::Response> {
    let sender_id = body.sender_id.as_ref().expect("user is authenticated");
    Ok(whoami::Response {
        user_id: sender_id.clone(),
    }
    .into())
}

/// # `POST /_matrix/client/r0/account/deactivate`
///
/// Deactivate this user's account
///
/// - Leaves all rooms and rejects all invitations
/// - Invalidates all access tokens
/// - Deletes all devices
/// - Removes ability to log in again
#[cfg_attr(
    feature = "conduit_bin",
    post("/_matrix/client/r0/account/deactivate", data = "<body>")
)]
pub async fn deactivate_route(
    db: State<'_, Database>,
    body: Ruma<deactivate::Request<'_>>,
) -> ConduitResult<deactivate::Response> {
    let sender_id = body.sender_id.as_ref().expect("user is authenticated");
    let device_id = body.device_id.as_ref().expect("user is authenticated");

    let mut uiaainfo = UiaaInfo {
        flows: vec![AuthFlow {
            stages: vec!["m.login.password".to_owned()],
        }],
        completed: Vec::new(),
        params: Default::default(),
        session: None,
        auth_error: None,
    };

    if let Some(auth) = &body.auth {
        let (worked, uiaainfo) = db.uiaa.try_auth(
            &sender_id,
            &device_id,
            auth,
            &uiaainfo,
            &db.users,
            &db.globals,
        )?;
        if !worked {
            return Err(Error::Uiaa(uiaainfo));
        }
    // Success!
    } else {
        uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
        db.uiaa.create(&sender_id, &device_id, &uiaainfo)?;
        return Err(Error::Uiaa(uiaainfo));
    }

    // Leave all joined rooms and reject all invitations
    for room_id in db
        .rooms
        .rooms_joined(&sender_id)
        .chain(db.rooms.rooms_invited(&sender_id))
    {
        let room_id = room_id?;
        let event = member::MemberEventContent {
            membership: member::MembershipState::Leave,
            displayname: None,
            avatar_url: None,
            is_direct: None,
            third_party_invite: None,
        };

        db.rooms.build_and_append_pdu(
            PduBuilder {
                event_type: EventType::RoomMember,
                content: serde_json::to_value(event).expect("event is valid, we just created it"),
                unsigned: None,
                state_key: Some(sender_id.to_string()),
                redacts: None,
            },
            &sender_id,
            &room_id,
            &db.globals,
            &db.sending,
            &db.account_data,
        )?;
    }

    // Remove devices and mark account as deactivated
    db.users.deactivate_account(&sender_id)?;

    Ok(deactivate::Response {
        id_server_unbind_result: ThirdPartyIdRemovalStatus::NoSupport,
    }
    .into())
}