use std::net::SocketAddr;

use axum::{
    extract::{Path, State, ConnectInfo},
    Json, http::StatusCode, Extension,
};
use chrono::{DateTime, TimeZone, Utc};
use rand::{distributions::Alphanumeric, Rng};
use serde::{Deserialize, Serialize};
use sqlx::Connection;

use crate::{
    api::{self, AppState, ApplicationError, UniversalResponseDto},
    db,
    entity::{
        availability::Availability,
        event::{Event, EventType},
    }
};

#[derive(Deserialize)]
pub struct CreateEventDto {
    from_date: Option<DateTime<Utc>>,
    to_date: Option<DateTime<Utc>>,
    name: String,
    description: Option<String>,
    event_type: String,
    duration: i32
}

#[derive(Serialize)]
pub struct EventDto {
    snowflake_id: String,
    from_date: Option<DateTime<Utc>>,
    to_date: Option<DateTime<Utc>>,
    name: String,
    description: Option<String>,
    event_type: String,
    duration: i32
}

#[derive(Deserialize)]
pub struct CreateAvailabilitiesDto {
    availabilities: Vec<CreateAvailabilityDto>,
    user_email: Option<String>,
    user_name: String,
}

#[derive(Deserialize, Clone)]
pub struct CreateAvailabilityDto {
    from_date: DateTime<Utc>,
    to_date: DateTime<Utc>,
}

#[derive(Serialize, Deserialize)]
pub struct AvailabilityDto {
    id: i64,
    from_date: DateTime<Utc>,
    to_date: DateTime<Utc>,
    user_name: String,
}

pub async fn create_event(
    State(app_state): State<AppState>,
    Json(dto): Json<CreateEventDto>,
) -> UniversalResponseDto<EventDto> {
    let event_uid_size = app_state.event_uid_size;

    let mut conn = match app_state.db_pool.acquire().await {
        Ok(c) => c,
        Err(e) => return api::internal_server_error(e.into()),
    };

    let res = conn
        .transaction(|txn| {
            Box::pin(async move {
                let uid: String = rand::thread_rng()
                    .sample_iter(&Alphanumeric)
                    .take(event_uid_size)
                    .map(char::from)
                    .collect();

                let event_type: EventType = dto.event_type.into();

                if matches!(event_type, EventType::Unknown) {
                    return Err(ApplicationError::new("Unknown event type, invalid variant.".to_string(), StatusCode::UNPROCESSABLE_ENTITY));
                }

                if matches!(event_type, EventType::SpecificDate) && dto.from_date.is_none() {
                    return Err(ApplicationError::new("SpecificDate event type supplied, but missing from_date".to_string(), StatusCode::UNPROCESSABLE_ENTITY));
                }

                if matches!(event_type, EventType::DateRange) {
                    match (dto.from_date, dto.to_date) {
                        (Some(from), Some(to)) => {
                            if from >= to {
                                return Err(ApplicationError::new("Supplied from_date is later than or equal to to_date".to_string(), StatusCode::UNPROCESSABLE_ENTITY));
                            }

                            if (to - from).num_days() < 1 {
                                return Err(ApplicationError::new("Difference between from_date and to_date is less than 1 day".to_string(), StatusCode::UNPROCESSABLE_ENTITY));
                            }

                            if (to - from).num_days() > 14 {
                                return Err(ApplicationError::new("Difference between from_date and to_date is greater than 14 days ( current supported maximum )".to_string(), StatusCode::UNPROCESSABLE_ENTITY));
                            }
                        },
                        _ => return Err(ApplicationError::new("DateRange event type supplied, but missing either from_date or to_date".to_string(), StatusCode::UNPROCESSABLE_ENTITY))
                    }

                }

                let event_id = db::insert_event_and_fetch_id(
                    txn,
                    Event {
                        id: -1,
                        snowflake_id: uid,
                        name: dto.name,
                        description: dto.description,
                        from_date: dto.from_date.map(|d| d.naive_utc()),
                        to_date: dto.to_date.map(|d| d.naive_utc()),
                        event_type,
                        duration: dto.duration
                    },
                )
                .await?;

                let event = db::fetch_event_by_id(txn, event_id).await?;

                Ok(EventDto {
                    snowflake_id: event.snowflake_id,
                    from_date: event.from_date.map(|d| Utc.from_utc_datetime(&d)),
                    to_date: event.to_date.map(|d| Utc.from_utc_datetime(&d)),
                    name: event.name,
                    description: event.description,
                    event_type: event.event_type.to_string(),
                    duration: event.duration
                })
            })
        })
        .await;

    api::ok::<EventDto>(res)
}

pub async fn fetch_event(
    State(app_state): State<AppState>,
    Path(event_snowflake_id): Path<String>,
) -> UniversalResponseDto<EventDto> {
    let mut conn = match app_state.db_pool.acquire().await {
        Ok(c) => c,
        Err(e) => return api::internal_server_error(e.into()),
    };

    let res = conn
        .transaction(|txn| {
            Box::pin(async move {
                let event = db::fetch_event_by_snowflake_id(txn, event_snowflake_id).await?;

                Ok(EventDto {
                    snowflake_id: event.snowflake_id,
                    from_date: event.from_date.map(|d| Utc.from_utc_datetime(&d)),
                    to_date: event.to_date.map(|d| Utc.from_utc_datetime(&d)),
                    name: event.name,
                    description: event.description,
                    event_type: event.event_type.to_string(),
                    duration: event.duration
                })
            })
        })
        .await;

    api::ok::<EventDto>(res)
}

pub async fn create_availabilities(
    State(app_state): State<AppState>,
    Path(event_snowflake_id): Path<String>,
    ConnectInfo(addr): ConnectInfo<SocketAddr>,
    Json(dto): Json<CreateAvailabilitiesDto>,
) -> UniversalResponseDto<()> {
    let mut conn = match app_state.db_pool.acquire().await {
        Ok(c) => c,
        Err(e) => return api::internal_server_error(e.into()),
    };

    let res = conn
        .transaction(|txn| {
            Box::pin(async move {
                let user_ip = format!("{}", addr.ip());

                let event = db::fetch_event_by_snowflake_id(txn, event_snowflake_id).await?;

                let current_availabilities = db::fetch_event_availabilities(txn, event.id).await?;

                let already_submitted = current_availabilities.iter().any(|a| {
                    (dto.user_email.is_some() && a.user_email.is_some() && a.user_email == dto.user_email)
                        || a.user_ip == user_ip
                        || a.user_name == dto.user_name
                });

                if already_submitted {
                    return Err(ApplicationError::new(
                        "Availability already submitted".to_string(),
                        StatusCode::UNPROCESSABLE_ENTITY
                    ));
                }

                // TODO: Do these in parallel.
                // At the moment, it would appear sqlx's transaction does not implement Clone, so it's not possible to execute these concurrently
                for a in dto.availabilities {
                    db::insert_availability_and_fetch_id(
                        txn,
                        Availability {
                            id: -1,
                            event_id: event.id,
                            from_date: a.from_date.naive_utc(),
                            to_date: a.to_date.naive_utc(),
                            user_email: dto.user_email.clone(),
                            user_ip: user_ip.clone(),
                            user_name: dto.user_name.clone(),
                        },
                    )
                    .await?;
                }

                Ok(())
            })
        })
        .await;

    api::ok::<()>(res)
}

pub async fn fetch_availabilities(
    State(app_state): State<AppState>,
    Path(event_snowflake_id): Path<String>,
) -> UniversalResponseDto<Vec<AvailabilityDto>> {
    let mut conn = match app_state.db_pool.acquire().await {
        Ok(c) => c,
        Err(e) => return api::internal_server_error(e.into()),
    };

    let res = conn
        .transaction(|txn| {
            Box::pin(async move {
                let availabilities =
                    db::fetch_event_availabilities_by_event_snowflake_id(txn, event_snowflake_id)
                        .await?;

                Ok(availabilities
                    .into_iter()
                    .map(|a| AvailabilityDto {
                        id: a.id,
                        user_name: a.user_name,
                        from_date: Utc.from_utc_datetime(&a.from_date),
                        to_date: Utc.from_utc_datetime(&a.to_date),
                    })
                    .collect())
            })
        })
        .await;

    api::ok::<Vec<AvailabilityDto>>(res)
}