use std::convert::TryInto;
use libpso::packet::ship::*;
use libpso::packet::messages::*;
use networking::serverstate::ClientId;
use crate::{SendShipPacket, ShipError, Clients};
use location::{ClientLocation};
use items::ClientItemId;
use items::state::{ItemState, ItemStateError};
use items::inventory::InventoryItemDetail;
use items::trade::TradeItem;
use entity::gateway::EntityGateway;
use pktbuilder as builder;
use items::tasks::trade_items;
use location::{AreaClient, RoomId};
use entity::item::Meseta;
use trade::{ClientTradeState, TradeState, TradeStatus};

pub const MESETA_ITEM_ID: ClientItemId = ClientItemId(0xFFFFFF01);
pub const OTHER_MESETA_ITEM_ID: ClientItemId = ClientItemId(0xFFFFFFFF);

#[derive(thiserror::Error, Debug, PartialEq, Eq)]
pub enum TradeError {
    #[error("no partner")]
    CouldNotFindTradePartner,
    #[error("invalid item id")]
    InvalidItemId(ClientItemId),
    #[error("item does not match id")]
    ClientItemIdDidNotMatchItem(ClientItemId, [u8; 16]),
    #[error("invalid stack {1}")]
    InvalidStackAmount(ClientItemId, usize),
    #[error("not in trade menu")]
    NotInTradeMenu,
    #[error("trade menu at an invalid point")]
    MismatchedStatus,
    #[error("no space in inventory")]
    NoInventorySpace,
    #[error("no space in stack")]
    NoStackSpace,
    #[error("invalid meseta amount")]
    InvalidMeseta,
    #[error("tried starting a trade while in one already")]
    ClientAlreadyInTrade,
    #[error("tried starting a trade while with player already in a trade")]
    OtherAlreadyInTrade,
    #[error("tried to trade item not specified in trade request")]
    SketchyTrade,
    #[error("items in trade window and items attempted to trade do not match")]
    MismatchedTradeItems,
}



async fn do_trade_action<F>(id: ClientId,
                            pkt: TradeRequest,
                            client_location: &ClientLocation,
                            target: u32,
                            this: &mut ClientTradeState,
                            other: &mut ClientTradeState,
                            action: F)
                            -> Result<Vec<(ClientId, SendShipPacket)>, anyhow::Error>
where
    F: Fn(&mut ClientTradeState, &mut ClientTradeState) -> Result<(), anyhow::Error>,
{
    Ok(match action(this, other) {
        Ok(_) => {
            client_location.get_all_clients_by_client(id).await?.into_iter()
                .filter(move |client| client.local_client.id() == target as u8)
                .map(move |client| {
                    (client.client, SendShipPacket::DirectMessage(DirectMessage::new(target, GameMessage::TradeRequest(pkt.clone()))))
                })
                .collect()
        },
        Err(_) => {
            // TODO: some sort of error logging?
            client_location.get_all_clients_by_client(id).await?.into_iter()
                .filter(move |client| client.local_client.id() == target as u8)
                .map(move |client| {
                    (client.client, SendShipPacket::CancelTrade(CancelTrade {}))
                })
                .chain(std::iter::once((id, SendShipPacket::CancelTrade(CancelTrade {}))))
                .collect()
        }
    })
}


// TODO: remove target
pub async fn trade_request(id: ClientId,
                           trade_request: TradeRequest,
                           target: u32,
                           client_location: &ClientLocation,
                           clients: &Clients,
                           item_state: &mut ItemState,
                           trades: &mut TradeState)
                           -> Result<Vec<(ClientId, SendShipPacket)>, anyhow::Error>
{
    let trade_request = trade_request.clone(); // TODO: make this function take ownership of packet
    match trade_request.trade {
        TradeRequestCommand::Initialize(ref act, _meseta) => {
            match act {
                TradeRequestInitializeCommand::Initialize => {
                    if trades.in_trade(&id) {
                        return Err(TradeError::ClientAlreadyInTrade.into())
                    }
                    let trade_partner = client_location.get_client_neighbors(id).await?
                        .into_iter()
                        .find(|ac| {
                            ac.local_client.id() == target as u8 //trade_request.client
                        })
                        .ok_or(TradeError::CouldNotFindTradePartner)?;
                    if trades.in_trade(&trade_partner.client) {
                        return Err(TradeError::OtherAlreadyInTrade.into())
                    }
                    trades.new_trade(&id, &trade_partner.client);
                    Ok(client_location.get_all_clients_by_client(id).await?.into_iter()
                       .filter(move |client| client.local_client.id() == target as u8)
                       .map(move |client| {
                           (client.client, SendShipPacket::DirectMessage(DirectMessage::new(target, GameMessage::TradeRequest(trade_request.clone()))))
                       })
                       .collect())
                },
                TradeRequestInitializeCommand::Respond => {
                    trades
                        .with(&id, |mut this, mut other| {
                            let trade_request = trade_request.clone();
                            async move {
                                do_trade_action(id, trade_request, client_location, target, &mut this, &mut other, |this, other| {
                                    if this.status == TradeStatus::ReceivedRequest && other.status == TradeStatus::SentRequest {
                                        this.status = TradeStatus::Trading;
                                        other.status = TradeStatus::Trading;
                                        Ok(())
                                    }
                                    else {
                                        Err(TradeError::MismatchedStatus.into())
                                    }
                                }).await
                            }}).await?
                }
            }
        },
        TradeRequestCommand::AddItem(item_id, amount) => {
            trades
                .with(&id, |mut this, mut other| {
                    let trade_request = trade_request.clone();
                    async move {
                        let inventory = clients.with(this.client(), |client| {
                            let item_state = item_state.clone();
                            Box::pin(async move {
                                item_state.get_character_inventory(&client.character).await
                            })}).await??;
                        do_trade_action(id, trade_request, client_location, target, &mut this, &mut other, |this, other| {
                            if this.status == TradeStatus::Trading && other.status == TradeStatus::Trading {
                                if ClientItemId(item_id) == MESETA_ITEM_ID {
                                    this.meseta += amount as usize;
                                }
                                else {
                                    let item = inventory.get_by_client_id(&ClientItemId(item_id)).ok_or_else(|| ItemStateError::InvalidItemId(ClientItemId(item_id)))?;
                                    
                                    match &item.item {
                                        InventoryItemDetail::Individual(_) => {
                                            this.items.push(TradeItem::Individual(ClientItemId(item_id)));
                                        },
                                        InventoryItemDetail::Stacked(stacked_item) => {
                                            if stacked_item.count() < amount as usize {
                                                return Err(TradeError::InvalidStackAmount(ClientItemId(item_id), amount as usize).into());
                                            }
                                            this.items.push(TradeItem::Stacked(ClientItemId(item_id), amount as usize));
                                        },
                                    }
                                }
                                Ok(())
                            }
                            else {
                                Err(TradeError::MismatchedStatus.into())
                            }
                        }).await
                    }}).await?
        },
        TradeRequestCommand::RemoveItem(item_id, amount) => {
            trades
                .with(&id, |mut this, mut other| {
                    let trade_request = trade_request.clone();
                    async move {
                        let inventory = clients.with(this.client(), |client| {
                            let item_state = item_state.clone();
                            Box::pin(async move {
                                item_state.get_character_inventory(&client.character).await
                            })}).await??;
                        do_trade_action(id, trade_request, client_location, target, &mut this, &mut other, |this, other| {
                            if this.status == TradeStatus::Trading && other.status == TradeStatus::Trading {
                                if ClientItemId(item_id) == MESETA_ITEM_ID {
                                    this.meseta -= amount as usize;
                                }
                                else {
                                    let item = inventory.get_by_client_id(&ClientItemId(item_id)).ok_or_else(|| ItemStateError::InvalidItemId(ClientItemId(item_id)))?;

                                    match &item.item {
                                        InventoryItemDetail::Individual(_) => {
                                            this.items.retain(|item| {
                                                item.item_id() != ClientItemId(item_id)
                                            })
                                        },
                                        InventoryItemDetail::Stacked(_stacked_item) => {
                                            let trade_item_index = this.items.iter()
                                                .position(|item| {
                                                    item.item_id() == ClientItemId(item_id)
                                                })
                                                .ok_or(TradeError::InvalidItemId(ClientItemId(item_id)))?;

                                            match this.items[trade_item_index].stacked().ok_or_else(|| ItemStateError::InvalidItemId(ClientItemId(item_id)))?.1.cmp(&(amount as usize)) {
                                                std::cmp::Ordering::Greater => {
                                                    *this.items[trade_item_index].stacked_mut().ok_or_else(|| ItemStateError::InvalidItemId(ClientItemId(item_id)))?.1 -= amount as usize;
                                                },
                                                std::cmp::Ordering::Equal => {
                                                    this.items.remove(trade_item_index);
                                                },
                                                std::cmp::Ordering::Less => {
                                                    return Err(TradeError::SketchyTrade.into())
                                                }
                                            }
                                        },
                                    }
                                }
                                Ok(())
                            }
                            else {
                                Err(TradeError::MismatchedStatus.into())
                            }
                            
                        }).await
                    }
                }).await?
        },
        TradeRequestCommand::Confirm => {
            trades
                .with(&id, |mut this, mut other| {
                    let trade_request = trade_request.clone();
                    async move {
                        do_trade_action(id, trade_request, client_location, target, &mut this, &mut other, |this, other| {
                            if status_is(&this.status, &[TradeStatus::Trading]) && status_is(&other.status, &[TradeStatus::Trading, TradeStatus::Confirmed]) {
                                this.status = TradeStatus::Confirmed;
                                Ok(())
                            }
                            else {
                                Err(TradeError::MismatchedStatus.into())
                            }
                        }).await
                    }
                }).await?
        },
        TradeRequestCommand::FinalConfirm => {
            trades
                .with(&id, |mut this, mut other| {
                    let trade_request = trade_request.clone();
                    async move {
                        do_trade_action(id, trade_request, client_location, target, &mut this, &mut other, |this, other| {
                            if this.status == TradeStatus::Confirmed && (other.status == TradeStatus::Confirmed || other.status == TradeStatus::FinalConfirm) {
                                this.status = TradeStatus::FinalConfirm;
                                Ok(())
                            }
                            else {
                                Err(TradeError::MismatchedStatus.into())
                            }
                        }).await
                    }
                }).await?
        },
        TradeRequestCommand::Cancel => {
            trades.remove_trade(&id).await;
            Ok(client_location.get_all_clients_by_client(id).await?.into_iter()
               .filter(move |client| client.local_client.id() == target as u8)
               .map(move |client| {
                   (client.client, SendShipPacket::CancelTrade(CancelTrade {}))
               })
               .chain(std::iter::once((id, SendShipPacket::CancelTrade(CancelTrade {}))))
               .collect())
        }
    }
}


fn status_is<const N: usize>(status: &TradeStatus, statuses: &[TradeStatus; N]) -> bool {
    statuses.iter().any(|s| s == status)
}

fn status_is_not<const N: usize>(status: &TradeStatus, statuses: &[TradeStatus; N]) -> bool {
    !status_is(status, statuses)
}

async fn inner_items_to_trade(id: ClientId,
                              items_to_trade: ItemsToTrade,
                              client_location: &ClientLocation,
                              clients: &Clients,
                              item_state: &mut ItemState,
                              trades: &mut TradeState)
                              -> Result<Vec<(ClientId, SendShipPacket)>, anyhow::Error>
{
    let pkts = trades
        .with(&id, |mut this, other| async move {
            if status_is_not(&this.status, &[TradeStatus::FinalConfirm]) || status_is_not(&other.status, &[TradeStatus::FinalConfirm, TradeStatus::ItemsChecked]) {
                return Err(anyhow::Error::from(ShipError::from(TradeError::MismatchedStatus)))
            }
            let other_client = other.client();
            let (this_inventory, other_inventory) = clients.with(this.client(), |client| {
                let item_state = item_state.clone();
                let clients = clients.clone();
                Box::pin(async move {
                    let this = item_state.get_character_inventory(&client.character).await?;
                    let other_inventory = clients.with(other_client, |client| {
                        let item_state = item_state.clone();
                        Box::pin(async move {
                            item_state.get_character_inventory(&client.character).await
                        })}).await??;
                    Ok::<_, anyhow::Error>((this, other_inventory))
                })}).await??;

            if items_to_trade.count as usize != (this.items.len() + usize::from(this.meseta != 0))  {
                return Err(TradeError::MismatchedTradeItems.into())
            }

            items_to_trade.items
                .iter()
                .take(items_to_trade.count as usize)
                .map(|item| {
                    if ClientItemId(item.item_id) == OTHER_MESETA_ITEM_ID {
                        if item.item_data[0] != 4 {
                            return Err(TradeError::InvalidItemId(ClientItemId(item.item_id)).into())
                        }
                        let amount = u32::from_le_bytes(item.item_data2);
                        let character_meseta = this_inventory.meseta;
                        let other_character_meseta = other_inventory.meseta;
                        if amount > character_meseta.0 {
                            return Err(TradeError::InvalidMeseta.into())
                        }
                        if (amount + other_character_meseta.0) > 999999 {
                            return Err(TradeError::InvalidMeseta.into())
                        }
                        if amount != this.meseta as u32{
                            return Err(TradeError::InvalidMeseta.into())
                        }
                        Ok(())
                    }
                    else {
                        let real_item = this_inventory.get_by_client_id(&ClientItemId(item.item_id))
                            .ok_or_else(|| ItemStateError::InvalidItemId(ClientItemId(item.item_id)))?;
                        let real_trade_item = this.items
                            .iter()
                            .find(|i| i.item_id() == ClientItemId(item.item_id))
                            .ok_or(TradeError::SketchyTrade)?;
                        let trade_item_bytes: [u8; 16] = item.item_data.iter()
                            .chain(item.item_data2.iter())
                            .cloned().collect::<Vec<u8>>()
                            .try_into()
                            .unwrap();
                        match &real_item.item {
                            InventoryItemDetail::Individual(_individual_inventory_item) => {
                                if real_item.item.as_client_bytes() == trade_item_bytes {
                                    Ok(())
                                }
                                else {
                                    Err(TradeError::ClientItemIdDidNotMatchItem(ClientItemId(item.item_id), trade_item_bytes).into())
                                }
                            },
                            InventoryItemDetail::Stacked(stacked_inventory_item) => {
                                if real_item.item.as_client_bytes()[0..4] == trade_item_bytes[0..4] {
                                    let amount = trade_item_bytes[5] as usize;
                                    if amount <= stacked_inventory_item.entity_ids.len() {
                                        if real_trade_item.stacked().ok_or(TradeError::SketchyTrade)?.1 == amount {
                                            Ok(())
                                        }
                                        else {
                                            Err(TradeError::InvalidStackAmount(real_item.item_id, amount).into())
                                        }
                                    }
                                    else {
                                        Err(TradeError::InvalidStackAmount(real_item.item_id, amount).into())
                                    }
                                }
                                else {
                                    Err(TradeError::ClientItemIdDidNotMatchItem(ClientItemId(item.item_id), trade_item_bytes).into())
                                }
                            }
                        }
                    }
                })
                .collect::<Result<Vec<_>, anyhow::Error>>()?;

            this.status = TradeStatus::ItemsChecked;
            if this.status == TradeStatus::ItemsChecked && other.status == TradeStatus::ItemsChecked {
                Ok(vec![
                    (this.client(), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})),
                    (other.client(), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})),
                ])
            }
            else {
                Ok(Vec::new())
            }
        }).await?;
    match pkts {
            Ok(pkts) => Ok(pkts),
            Err(err) => {
                log::warn!("trade error: {:?}", err);
                let (_this, other) = trades.remove_trade(&id).await;
                Ok(client_location.get_all_clients_by_client(id).await?.into_iter()
                   .filter(move |client| other.as_ref().map(|other| client.client == other.client() ).unwrap_or_else(|| false))
                   .map(move |client| {
                       (client.client, SendShipPacket::CancelTrade(CancelTrade {}))
                   })
                   .chain(std::iter::once((id, SendShipPacket::CancelTrade(CancelTrade {}))))
                   .collect())
            }
        }
}

pub async fn items_to_trade(id: ClientId,
                            items_to_trade_pkt: ItemsToTrade,
                            client_location: &ClientLocation,
                            clients: &Clients,
                            item_state: &mut ItemState,
                            trades: &mut TradeState)
                            -> Result<Vec<(ClientId, SendShipPacket)>, anyhow::Error>
{
    let t = inner_items_to_trade(id, items_to_trade_pkt, client_location, clients, item_state, trades).await;
    match t {
        Ok(p) => Ok(p),
        Err(err) => {
            log::warn!("atrade error: {:?}", err);
            let (_this, other) = trades.remove_trade(&id).await;
            Ok(client_location.get_all_clients_by_client(id).await?.into_iter()
               .filter(move |client| other.as_ref().map(|other| client.client == other.client()).unwrap_or_else(|| false))
               .map(move |client| {
                   (client.client, SendShipPacket::CancelTrade(CancelTrade {}))
               })
               .chain(std::iter::once((id, SendShipPacket::CancelTrade(CancelTrade {}))))
               .collect())
        }
    }
}

async fn trade_confirmed_inner<EG>(id: ClientId,
                                       entity_gateway: &mut EG,
                                       client_location: &ClientLocation,
                                       clients: &Clients,
                                       item_state: &mut ItemState,
                                       trades: &mut TradeState)
                                       -> Result<Vec<(ClientId, SendShipPacket)>, anyhow::Error>
where
    EG: EntityGateway + Clone + 'static,
{
    enum TradeReady/*<'a>*/ {
        OnePlayer,
        BothPlayers(RoomId,
                    (AreaClient, ClientTradeState),
                    (AreaClient, ClientTradeState)),
                    //(AreaClient, &'a crate::ship::ship::ClientState, crate::ship::trade::ClientTradeState),
                    //(AreaClient, &'a crate::ship::ship::ClientState, crate::ship::trade::ClientTradeState)),
    }

    let trade = trades
       .with(&id, |mut this, other| {
           async move {
               if status_is_not(&this.status, &[TradeStatus::ItemsChecked]) || status_is_not(&other.status, &[TradeStatus::ItemsChecked, TradeStatus::TradeComplete]) {
                   return Err(anyhow::Error::from(ShipError::TradeError(TradeError::MismatchedStatus)))
               }
               this.status = TradeStatus::TradeComplete;
               
               if this.status == TradeStatus::TradeComplete && other.status == TradeStatus::TradeComplete {
                   let this_local_client = client_location.get_local_client(this.client()).await?;
                   let other_local_client = client_location.get_local_client(other.client()).await?;
                   let room_id = client_location.get_room(id).await?;
                   
                   Ok(TradeReady::BothPlayers(room_id,
                                              (this_local_client, /*this_client, */this.clone()),
                                              (other_local_client, /*other_client, */other.clone())))
               }
               else {
                   Ok(TradeReady::OnePlayer)
               }
           }
       }).await??;

    match trade {
        TradeReady::OnePlayer => {
            Ok(Vec::new())
        },
        TradeReady::BothPlayers(_room_id, (this_local_client, /*this_client,*/ this), (other_local_client, /*other_client,*/ other)) => {
            let remove_item_packets = this.items
                .clone()
                .into_iter()
                .map(move |item| {
                    (this_local_client, item)
                })
                .chain(other.items
                       .clone()
                       .into_iter()
                       .map(move |item| {
                           (other_local_client, item)
                       }))
                .map(|(client, item)| {
                    GameMessage::PlayerNoLongerHasItem(builder::message::player_no_longer_has_item(client, item.item_id(), item.amount() as u32))
                });

            let this_items = this.items.clone();
            let other_items = other.items.clone();
            let (this_new_items, other_new_items) = clients.with_many(
                [this_local_client.client, other_local_client.client],
                |[this_client, other_client]| {
                    let mut entity_gateway = entity_gateway.clone();
                    let mut item_state = item_state.clone();
                    Box::pin(async move {
                        trade_items(&mut item_state,
                                    &mut entity_gateway,
                                    (&this_local_client, &this_client.character, &this_items, Meseta(this.meseta as u32)),
                                    (&other_local_client, &other_client.character, &other_items, Meseta(other.meseta as u32))).await
                    })}).await??;
            
            let create_item_packets = this_new_items
                .into_iter()
                .map(move |item| {
                    (this_local_client, item)
                })
                .chain(other_new_items
                       .into_iter()
                       .map(move |item| {
                           (other_local_client, item)
                       }))
                .map(|(client, item)| {
                    match item.item {
                        InventoryItemDetail::Individual(individual_item) => {
                            GameMessage::CreateItem(builder::message::create_individual_item(client, item.item_id, &individual_item))
                        },
                        InventoryItemDetail::Stacked(stacked_item) => {
                            GameMessage::CreateItem(builder::message::create_stacked_item(client, item.item_id, &stacked_item.tool, stacked_item.count()))
                        }
                    }
                });

            let meseta_packets = vec![(this_local_client, other_local_client, this.meseta), (other_local_client, this_local_client, other.meseta)]
                .into_iter()
                .filter(|(_, _, meseta)| *meseta != 0)
                .flat_map(|(this, other, meseta)| {
                    [
                        GameMessage::PlayerNoLongerHasItem(builder::message::player_no_longer_has_item(this, MESETA_ITEM_ID, meseta as u32)),
                        GameMessage::CreateItem(builder::message::create_meseta(other, meseta)),
                    ]
                });

            let clients_in_room = client_location.get_all_clients_by_client(id).await?;
            let traded_item_packets = remove_item_packets
                .chain(create_item_packets)
                .chain(meseta_packets)
                .flat_map(move |packet| {
                    clients_in_room
                        .clone()
                        .into_iter()
                        .filter_map(move |client| {
                            match packet {
                                GameMessage::PlayerNoLongerHasItem(ref no_longer) => {
                                    if client.local_client == no_longer.client {
                                        None
                                    }
                                    else {
                                        Some((client.client, SendShipPacket::Message(Message::new(packet.clone()))))
                                    }
                                }
                                _ => Some((client.client, SendShipPacket::Message(Message::new(packet.clone()))))
                            }
                        })
                });

            let close_trade = vec![
                (this.client(), SendShipPacket::TradeSuccessful(TradeSuccessful::default())),
                (other.client(), SendShipPacket::TradeSuccessful(TradeSuccessful::default()))
            ].into_iter();
            Ok(traded_item_packets.chain(close_trade).collect())
        }
    }
}


pub async fn trade_confirmed<EG>(id: ClientId,
                                 entity_gateway: &mut EG,
                                 client_location: &ClientLocation,
                                 clients: &Clients,
                                 item_state: &mut ItemState,
                                 trades: &mut TradeState)
                                 -> Result<Vec<(ClientId, SendShipPacket)>, anyhow::Error>
where
    EG: EntityGateway + Clone + 'static,
{
    match trade_confirmed_inner(id, entity_gateway, client_location, clients, item_state, trades).await {
        Ok(result) => Ok(result),
        Err(_err) => {
            let (_this, other) = trades.remove_trade(&id).await;
            Ok(client_location.get_all_clients_by_client(id).await?.into_iter()
               .filter(move |client| other.as_ref().map(|other| client.client == other.client()).unwrap_or_else(|| false))
               .map(move |client| {
                   (client.client, SendShipPacket::CancelTrade(CancelTrade {}))
               })
               .chain(std::iter::once((id, SendShipPacket::CancelTrade(CancelTrade {}))))
               .collect())
        }
    }
}