use leptos::*;
use validator::Validate;
use crate::backend::data::{ApiResponse, CrReservation, Reservation, PublicFormData, ResSumWithItems};
use crate::components::data_form::ForValidation;
use cfg_if::cfg_if;
use chrono::{NaiveDate, NaiveTime};

cfg_if! { if #[cfg(feature = "ssr")] {
    use sqlx::{Postgres, Transaction, query};
    use sqlx::query_as;
    use sqlx::Error;
    use uuid::Uuid;
    use std::ops::DerefMut;
    use std::str::FromStr;
    use std::collections::HashMap;
    use log::warn;
    use log::info;
    use crate::backend::data::{ReservationSum, ReservationState, ResWithProperty, Customer, Message, MessageType, ChartData, ResAllView};
    use crate::backend::get_pool;
    use crate::backend::get_mailing;
    use crate::backend::mail::MailMessage;
    use crate::backend::mail::get_message;
    use crate::error::AppError;
    use sqlx::PgPool;
    use crate::backend::user::admin_email;
    use crate::backend::user::emails_for_notify;
    use crate::locales::trl;
    use rust_decimal::prelude::ToPrimitive;

    async fn find_sum_by_uuid(uuid: &Uuid, tx: &mut Transaction<'_, Postgres>) -> Result<ReservationSum, Error> {
        let reservation = query_as::<_, ReservationSum>("SELECT * FROM reservation_sum WHERE uuid = $1")
            .bind(uuid)
            .fetch_one(tx.deref_mut())
            .await?;

        Ok(reservation)
    }

    async fn reservations_for_day(day: &NaiveDate) -> Result<Vec<Reservation>, ServerFnError> {
        let pool = get_pool().await?;
        let reservations = query_as::<_, Reservation>("SELECT * FROM reservation \
                        JOIN reservation_sum on reservation.summary=reservation_sum.id \
                        WHERE reservation_sum.date=$1 AND reservation_sum.state <> 'Canceled'")
            .bind(day)
            .fetch_all(&pool)
            .await;

        if let Err(e) = reservations {
            if matches!(e, Error::RowNotFound) {
                Ok(vec![])
            } else {
                Err(e.into())
            }
        } else {
            Ok(reservations?)
        }
    }

    async fn reservation_by_uuid(uuid: Uuid) -> Result<ResSumWithItems, ServerFnError> {
        let pool = get_pool().await?;
        let summary = query_as::<_, ReservationSum>("SELECT * FROM reservation_sum WHERE uuid = $1")
            .bind(uuid)
            .fetch_one(&pool)
            .await?;
        let sum_id = summary.id();
        let cust_id = summary.customer;

        Ok(ResSumWithItems{
            summary,
            customer: customer_for_reservation(cust_id, &pool).await?,
            reservations: items_for_reservation(sum_id, &pool).await?
        })
    }

    async fn items_for_reservation(id: i32, pool: &PgPool) -> Result<Vec<ResWithProperty>, ServerFnError> {
        Ok(query_as::<_, ResWithProperty>(
                "SELECT r.id, r.from, r.to, r.property, r.summary, p.name, p,description \
                    FROM reservation as r  \
                    JOIN property as p ON r.property = p.id WHERE r.summary = $1")
            .bind(id)
            .fetch_all(pool)
            .await?)
    }

    async fn customer_for_reservation(id: i32, pool: &PgPool) -> Result<Customer, ServerFnError> {
        Ok(query_as::<_, Customer>("SELECT * FROM customer WHERE id = $1")
            .bind(id)
            .fetch_one(pool)
            .await?)
    }

    async fn reservations_in_range(from: &NaiveDate, to: &NaiveDate, state: Option<ReservationState>) -> Result<Vec<ResSumWithItems>, ServerFnError> {
        let pool = get_pool().await?;
        let view = if let Some(s) = state {
            query_as::<_, ResAllView>(
                "SELECT r.from, r.to, p.name, p.description, s.*, c.full_name, c.email, c.phone \
                    FROM reservation r \
                    JOIN property p on p.id = r.property \
                    JOIN reservation_sum s on s.id = r.summary \
                    JOIN customer c on c.id = s.customer \
                    WHERE s.date >= $1 AND s.date <= $2 AND s.state = $3 \
                    ORDER BY s.id, s.date")
                .bind(from)
                .bind(to)
                .bind(s)
                .fetch_all(&pool)
                .await
        } else {
            query_as::<_, ResAllView>(
                "SELECT r.from, r.to, p.name, p.description, s.*, c.full_name, c.email, c.phone \
                    FROM reservation r \
                    JOIN property p on p.id = r.property \
                    JOIN reservation_sum s on s.id = r.summary \
                    JOIN customer c on c.id = s.customer \
                    WHERE s.date >= $1 AND s.date <= $2 \
                    ORDER BY s.id, s.date")
                .bind(from)
                .bind(to)
                .fetch_all(&pool)
                .await
        };

        let view = if let Err(ref e) = view {
            if matches!(e, Error::RowNotFound) {
                vec![]
            } else {
                view?
            }
        } else {
            view?
        };

        if view.is_empty() {
            return Ok(vec![])
        }

        let mut ret = view.into_iter().fold(HashMap::new(), |mut m, v| {
            m.entry(v.summary.id())
                .and_modify(|i: &mut (ReservationSum, Customer, Vec<ResWithProperty>)| i.2.push(v.reservation.clone()))
                .or_insert((v.summary, v.customer, vec![v.reservation]));
            m
        }).into_iter().map(|i| {
            ResSumWithItems {
                summary: i.1.0,
                customer: i.1.1,
                reservations: i.1.2
            }
        }).collect::<Vec<_>>();

        ret.sort_by(|a, b| a.summary.date.cmp(&b.summary.date) );
        Ok(ret)
    }

    async fn set_state(uuid: Uuid, state: ReservationState) -> Result<(), ServerFnError> {
        let pool = get_pool().await?;
        query("UPDATE reservation_sum SET state = $1 WHERE uuid = $2")
            .bind(state)
            .bind(uuid)
            .execute(&pool)
            .await?;

        Ok(())
    }

    async fn notify_new_all(admin_mail: String, reservation: &ResSumWithItems) -> Result<(), AppError> {
        let mailing = get_mailing().await?;
        let msg = get_message(MessageType::NewReservation).await?;

        for m in emails_for_notify().await? {
            mailing.send_mail(MailMessage::new(admin_mail.clone(), m, msg.clone(), reservation)).await?;
        }

        Ok(())
    }

    async fn notify_new(uuid: Uuid) -> Result<(), AppError> {
        let mailing = get_mailing().await?;
        let msg = get_message(MessageType::NewReservationCust).await?;
        let reservation = reservation_by_uuid(uuid).await?;
        let admin_mail = admin_email().await;

        if admin_mail.is_none() {
            return Err(AppError::MailSendError("No admin mail".to_string()))
        }

        mailing.send_mail(MailMessage::new(admin_mail.clone().unwrap(), reservation.customer.email.clone(), msg, &reservation)).await?;
        notify_new_all(admin_mail.unwrap(), &reservation).await
    }

    async fn send_notify(uuid: Uuid, msg: Message) -> Result<(), AppError> {
        let mailing = get_mailing().await?;
        let reservation = reservation_by_uuid(uuid).await?;
        let admin_mail = admin_email().await;

        if admin_mail.is_none() {
            return Err(AppError::MailSendError("No admin mail".to_string()))
        }

        mailing.send_mail(MailMessage::new(admin_mail.unwrap(), reservation.customer.email.clone(), msg, &reservation)).await
    }

    async fn notify_approve(uuid: Uuid) -> Result<(), AppError> {
        send_notify(uuid, get_message(MessageType::ReservationApp).await?).await
    }

    async fn notify_cancel(uuid: Uuid) -> Result<(), AppError> {
        send_notify(uuid, get_message(MessageType::ReservationCanceled).await?).await
    }

    async fn month_chart_data(year: i32) -> Result<Vec<ChartData>, AppError> {
        let pool = get_pool().await?;
        let data = query_as::<_, ChartData>(
            "SELECT count(id) as count, date_part('month', date) as period \
                FROM reservation_sum \
                WHERE date_part('year', date) = $1 GROUP BY period ORDER BY period")
            .bind(year)
            .fetch_all(&pool)
            .await?;

        Ok(data)
    }

    async fn year_chart_data() -> Result<Vec<ChartData>, AppError> {
        let pool = get_pool().await?;
        let data = query_as::<_, ChartData>(
            "SELECT count(id) as count, date_part('year', date) as period FROM reservation_sum GROUP BY period ORDER BY period")
            .fetch_all(&pool)
            .await?;

        Ok(data)
    }
}}

#[server]
pub async fn get_public_form_data(day: NaiveDate) -> Result<ApiResponse<Vec<PublicFormData>>, ServerFnError> {
    use crate::backend::opening_hours::hours_for_day;
    use crate::backend::property::get_props;
    use chrono::Datelike;

    let hours = hours_for_day(day.weekday()).await?;
    let props = get_props(Some("active = true".to_string())).await?;
    let reservations = reservations_for_day(&day).await?;

    info!("Loading public form data");

    Ok(ApiResponse::Data(props.into_iter().map(|p| PublicFormData {
        property: p,
        hours: hours.clone(),
        reservations: reservations.clone()
    }).collect::<Vec<_>>()))
}

pub fn is_reserved(reservations: &Vec<Reservation>, time: &NaiveTime, property: i32) -> bool {
    for r in reservations {
        if r.property == property && &r.from <= time && time < &r.to {
            return true
        }
    }
    false
}

#[server]
pub async fn create_reservation(reservation: CrReservation, pow: String) -> Result<ApiResponse<NaiveDate>, ServerFnError> {
    use crate::backend::get_pool;
    use crate::backend::customer::find_customer_by_email;
    use crate::backend::customer::sync_customer_data;
    use crate::backend::customer::create_customer;
    use crate::backend::customer::remember_customer;
    use crate::backend::property::get_prop_by_id;
    use crate::backend::data::{TmCheck, ReservationState, Reservations};
    use std::collections::HashMap;
    use crate::error::AppError;
    use chrono::Local;
    use sqlx::query;
    use rust_decimal::Decimal;
    use leptos_captcha::spow::pow::Pow;

    Pow::validate(&pow)?;

    if reservation.remember() {
        remember_customer(Customer::new(
            0,
            reservation.full_name().to_string(),
            reservation.email().to_string(),
            reservation.phone().to_string(),
            0)).await?;
    }

    let slots = reservation.slots().iter().fold(HashMap::new(), |mut map, s| {
        let slot_str = s.split("|").collect::<Vec<_>>();
        map.entry(i32::from_str(slot_str.get(1).unwrap_or(&"")).unwrap_or(0))
            .and_modify(|slot: &mut Vec<Result<TmCheck, AppError>>| slot.push(TmCheck::from_str(slot_str.get(0).unwrap_or(&""))))
            .or_insert(vec![TmCheck::from_str(slot_str.get(0).unwrap_or(&""))]);
        map
    });

    let res_for_day = reservations_for_day(&reservation.date()).await?;
    let mut reservations = Reservations::new();
    let mut price = Decimal::from(0);
    for sl in slots {
        let mut checks = sl.1.clone();
        checks.sort();
        let property = get_prop_by_id(sl.0).await?;
        for c in checks {
            reservations.add_slot(&c.clone()?, sl.0);
            price = price + property.price;
            if is_reserved(&res_for_day, &c?.from, sl.0) {
                return Ok(ApiResponse::Error("Slot and time already booked".to_string()))
            }
        }
    }

    let pool = get_pool().await?;
    let mut tx = pool.begin().await?;

    let customer = if let Some(c) = find_customer_by_email(reservation.email(), &mut tx).await {
        sync_customer_data(&c, reservation.full_name(), reservation.phone(), &mut tx).await?
    } else {
        create_customer(reservation.full_name(), reservation.email(), reservation.phone(), &mut tx).await?
    };

    let res_uuid = Uuid::new_v4();
    query("INSERT INTO reservation_sum(uuid, date, customer, price, state, note, date_create) VALUES($1, $2, $3, $4, $5, $6, $7)")
        .bind(res_uuid)
        .bind(reservation.date())
        .bind(customer.id())
        .bind(price)
        .bind(ReservationState::New)
        .bind(reservation.note())
        .bind(Local::now().date_naive())
        .execute(tx.deref_mut())
        .await?;
    let sum = find_sum_by_uuid(&res_uuid, &mut tx).await?;

    for r in reservations.reservations() {
        query(r#"INSERT INTO reservation("from", "to", property, summary) VALUES($1, $2, $3, $4)"#)
            .bind(r.from)
            .bind(r.to)
            .bind(r.property)
            .bind(sum.id())
            .execute(tx.deref_mut())
            .await?;
    }

    tx.commit().await?;

    if let Err(e) = notify_new(res_uuid).await {
        warn!("Notification not send: {}", e);
    }

    info!("Reservation created");

    Ok(ApiResponse::Data(reservation.date()))
}

impl ForValidation for CreateReservation {
    fn entity(&self) -> &dyn Validate {
        &self.reservation
    }
}

#[server]
pub async fn get_new_reservations() -> Result<ApiResponse<Vec<ResSumWithItems>>, ServerFnError> {
    use crate::perm_check;
    use chrono::{Days, Local};
    use crate::backend::data::ReservationState;

    perm_check!(is_logged_in);

    Ok(ApiResponse::Data(reservations_in_range(&Local::now().date_naive(),
                                               &Local::now().checked_add_days(Days::new(7)).unwrap().date_naive(),
                                               Some(ReservationState::New)).await?))
}

#[server]
pub async fn get_next_reservations() -> Result<ApiResponse<Vec<ResSumWithItems>>, ServerFnError> {
    use crate::perm_check;
    use chrono::{Days, Local};
    use crate::backend::data::ReservationState;

    perm_check!(is_logged_in);

    Ok(ApiResponse::Data(reservations_in_range(&Local::now().date_naive(),
                                               &Local::now().checked_add_days(Days::new(7)).unwrap().date_naive(),
                                               Some(ReservationState::Approved)).await?))
}

#[cfg(feature = "ssr")]
fn num_days(month: u32, year: i32) -> i64 {
    if month == 12 {
        NaiveDate::from_ymd_opt(year + 1, 1, 1).unwrap()
    } else {
        NaiveDate::from_ymd_opt(year, month + 1, 1).unwrap()
    }.signed_duration_since(NaiveDate::from_ymd_opt(year, month, 1).unwrap())
        .num_days()
}

#[server]
pub async fn get_reservations_for_month(month: u32, year: i32) -> Result<ApiResponse<Vec<ResSumWithItems>>, ServerFnError> {
    let data = reservations_in_range(&NaiveDate::from_ymd_opt(year, month, 1).unwrap(),
        &NaiveDate::from_ymd_opt(year, month, num_days(month, year).to_u32().unwrap_or_default()).unwrap(),
        None).await?;

    Ok(ApiResponse::Data(data))
}

#[server]
pub async fn approve(uuid: String) -> Result<ApiResponse<()>, ServerFnError> {
    use crate::perm_check;
    use crate::backend::data::ReservationState;

    perm_check!(is_logged_in);
    let uuid = Uuid::parse_str(&uuid)?;
    set_state(uuid, ReservationState::Approved).await?;

    if let Err(e) = notify_approve(uuid).await {
        warn!("Approve notification not send: {}", e);
    }

    Ok(ApiResponse::Data(()))
}

#[server]
pub async fn cancel(uuid: String)  -> Result<ApiResponse<()>, ServerFnError> {
    use crate::perm_check;
    use crate::backend::data::ReservationState;

    perm_check!(is_logged_in);
    let uuid = Uuid::parse_str(&uuid)?;
    set_state(uuid, ReservationState::Canceled).await?;

    if let Err(e) = notify_cancel(uuid).await {
        warn!("Cancel notification not send: {}", e);
    }

    Ok(ApiResponse::Data(()))
}

#[cfg(feature = "ssr")]
fn chart(data: &Vec<ChartData>, title: &str, month: bool) -> String {
    use charts_rs::{BarChart, THEME_ANT};
    use base64::Engine;
    use base64::prelude::BASE64_STANDARD;
    use std::ops::Add;
    use chrono::Month;

    let mut chart = BarChart::new_with_theme(
        vec![("Bookings", data.iter().map(|d| d.count.to_f32().unwrap_or_default()).collect()).into()],
        if month {
            data.iter().map(|d| trl(Month::try_from(d.period.to_u8().unwrap_or_default()).unwrap_or(Month::January).name())()).collect()
        } else {
            data.iter().map(|d| d.period.to_string()).collect()
        },
        THEME_ANT);
    chart.title_text = title.to_string();
    chart.legend_show = Some(false);
    chart.series_label_font_size = 8.0;
    chart.x_axis_font_size = 8.0;
    chart.title_font_size = 11.0;
    chart.font_family = "Arial".to_string();
    chart.y_axis_configs[0].axis_font_size = 8.0;


    chart.width = 300.0;
    chart.height = 150.0;

    "data:image/svg+xml;base64,".to_string().add(&BASE64_STANDARD.encode(&chart.svg().unwrap_or_default()))
}

#[server]
pub async fn month_chart(year: i32) -> Result<ApiResponse<String>, ServerFnError> {
    use crate::perm_check;
    perm_check!(is_logged_in);

    let data = month_chart_data(year).await?;
    Ok(ApiResponse::Data(chart(&data, &trl("Month bookings")(), true)))
}

#[server]
pub async fn year_chart() -> Result<ApiResponse<String>, ServerFnError> {
    use crate::perm_check;
    perm_check!(is_logged_in);

    let data = year_chart_data().await?;
    Ok(ApiResponse::Data(chart(&data, &trl("Year bookings")(), false)))
}

#[server]
pub async fn years() -> Result<Vec<i32>, ServerFnError> {
    let pool = get_pool().await?;

    let data: Result<Vec<(f64,)>, Error> = query_as("SELECT DISTINCT date_part('year', date) as year FROM reservation_sum ORDER BY year DESC")
        .fetch_all(&pool)
        .await;

    Ok(data?.into_iter().map(|d| d.0.to_i32().unwrap_or_default()).collect())
}

#[server]
pub async fn months(year: i32) -> Result<Vec<u32>, ServerFnError> {
    let pool = get_pool().await?;

    let data: Result<Vec<(f64,)>, Error> = query_as("SELECT DISTINCT date_part('month', date) as month \
            FROM reservation_sum \
            WHERE date_part('year', date) = $1 \
            ORDER BY month DESC")
        .bind(year)
        .fetch_all(&pool)
        .await;

    Ok(data?.into_iter().map(|d| d.0.to_u32().unwrap_or_default()).collect())
}

#[server]
pub async fn reservations_in_month(year: i32, month: u32) -> Result<ApiResponse<Vec<ResSumWithItems>>, ServerFnError> {
    use crate::perm_check;
    perm_check!(is_logged_in);

    let ret = reservations_in_range(&NaiveDate::from_ymd_opt(year, month, 1).ok_or(ServerFnError::new("Cannot parse date"))?,
                          &NaiveDate::from_ymd_opt(year, month, num_days(month, year).to_u32().unwrap_or_default())
                              .ok_or(ServerFnError::new("Cannot parse date"))?,
                          None)
        .await?;

    Ok(ApiResponse::Data(ret))
}