using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using ReallifeGamemode.Database.Entities;
using ReallifeGamemode.Server.Core.API;
using ReallifeGamemode.Server.Types;
using ReallifeGamemode.Server.Core.Extensions;
using System.Linq;
using ReallifeGamemode.Database.Models;
using ReallifeGamemode.Server.Common;

namespace ReallifeGamemode.Server.Core.Managers
{
  public class HouseManager : Script
  {
    public static readonly Dictionary<int, IMarker> houseMarkers = new Dictionary<int, IMarker>();
    private readonly Dictionary<int, ITextLabel> houseLabels = new Dictionary<int, ITextLabel>();
    private readonly Dictionary<int, IColShape> houseColShapes = new Dictionary<int, IColShape>();
    private readonly Dictionary<int, IBlip> houseBlips = new Dictionary<int, IBlip>();

    private readonly Dictionary<int, List<IPlayer>> playerInColShape = new Dictionary<int, List<IPlayer>>();

    public HouseManager()
    {
      EventHandler.RegisterClientEvent("House_BuyHouse", HouseManagerBuyHouseEvent);
      EventHandler.RegisterClientEvent("House_SetRentalFee", HouseManagerSetRentalFeeEvent);
      EventHandler.RegisterClientEvent("House_CancelUserRental", HouseManagerCancelUserRentalEvent);
      EventHandler.RegisterClientEvent("House_RentInHouse", HouseManagerRentInHouseEvent);
      EventHandler.RegisterClientEvent("House_CancelOwnRental", HouseManagerCancelOwnRentalEvent);
      EventHandler.RegisterClientEvent("House_SellHouse", HouseManagerSellHouseEvent);
      EventHandler.RegisterClientEvent("House_WithdrawMoney", HouseManagerWithdrawMoneyEvent);

      LoadHouses();
    }

    private void LoadHouses()
    {
      using var dbContext = GetDbContext();
      var houses = dbContext.Houses.Include(h => h.Owner).Include(h => h.BankAccount);
      foreach (House house in houses)
      {
        LoadHouse(house);
      }
      dbContext.SaveChanges();
    }

    public void LoadHouse(House house)
    {
      Position housePos = new Position(house.X, house.Y, house.Z);

      playerInColShape[house.Id] = new List<IPlayer>();

      houseMarkers[house.Id] = Api.Marker.CreateMarker(MarkerType.VerticalCylinder, housePos.Subtract(new Position(0, 0, 1.7)), new Position(), new Position(), 1.6f, Color.White);
      string text = $"~g~Zum Verkauf~s~(ID: {house.Id})\n~s~{house.Type}\nPreis: ~y~{(house.Price == 0 ? "~r~Nicht verkäuflich" : house.Price.ToMoneyString())}";

      if (house.OwnerId != null)
      {
        text = $"{house.Type}~s~ (ID: {house.Id})\n~s~Besitzer: ~y~{house.Owner.Name}";

        if (house.RentalFee != 0)
        {
          text += $"\n~s~Mietpreis: ~g~{house.RentalFee.ToMoneyString()}";
        }
      }
      else
      {
        //houseBlips[house.Id] = NAPI.Blip.CreateBlip(40, house.Position, 0.7f, 11, "Haus", shortRange: true); too many blips
        houseBlips[house.Id] = Api.Blip.CreateBlip(40, house.Position, "Haus", 11, 0.5f, shortRange: true);
      }

      houseLabels[house.Id] = Api.TextLabel.CreateTextLabel(text, housePos, 10f, 1f, 0, new Color(255, 255, 255));

      if (house.Price != 0 || house.RentalFee != 0)
      {
        houseColShapes[house.Id] = Api.ColShape.CreateCyclinder(housePos.Subtract(new Position(0, 0, 2)), 4.0f, 2f);

        houseColShapes[house.Id].OnEntityEnter += HouseManager_OnEntityEnterColShape;
        houseColShapes[house.Id].OnEntityExit += HouseManager_OnEntityExitColShape;
      }

      if (house.BankAccount == null)
      {
        house.BankAccount = new HouseBankAccount();
      }
    }

    private void HouseManager_OnEntityExitColShape(IColShape colShape, IPlayer client)
    {
      if (!client.IsLoggedIn() || client.IsInVehicle) return;
      if (!houseColShapes.ContainsValue(colShape))
      {
        return;
      }
      int houseId = houseColShapes.Where(p => p.Value.Handle == colShape.Handle).FirstOrDefault().Key;
      playerInColShape[houseId].Remove(client);

      client.TriggerEvent("CloseHouseMenu");
    }

    private void HouseManager_OnEntityEnterColShape(IColShape colShape, IPlayer client)
    {
      if (!client.IsLoggedIn() || client.IsInVehicle) return;
      if (!houseColShapes.ContainsValue(colShape))
      {
        return;
      }

      using (var dbContext = GetDbContext())
      {
        int houseId = houseColShapes.Where(p => p.Value.Handle == colShape.Handle).FirstOrDefault().Key;
        playerInColShape[houseId].Add(client);
        House house = GetHouseById(houseId, dbContext);
        User user = client.GetUser(dbContext);

        client.TriggerEvent("ShowHouseMenu");
        SendPlayerHouseData(client, house);
      }
    }

    public House GetHouseById(int id, DatabaseContext dbContext)
    {
      return dbContext.Houses.Where(h => h.Id == id)
        .Include(h => h.Owner)
        .Include(h => h.Rentals)
          .ThenInclude(hR => hR.User)
        .Include(h => h.BankAccount)
        .FirstOrDefault();
    }

    private void SendPlayerHouseData(IPlayer player, House house)
    {
      using var dbContext = GetDbContext();
      User user = player.GetUser(dbContext);
      var userHouseStatus = -1;

      if (house.OwnerId == null) userHouseStatus = 0;
      else if (house.OwnerId == user?.Id) userHouseStatus = 1;
      else if (house.Rentals.Where(h => h.UserId == user.Id).Any()) userHouseStatus = 2;

      var rentals = house.Rentals;

      var newHouse = new
      {
        OwnerName = house.Owner?.Name,
        house.RentalFee,
        house.Price,
        house.Type,
        Rentals = rentals.Select(r => r.User.Name),
        house.BankAccount.Balance
      };

      player.TriggerEvent("SetHouseData", newHouse.SerializeJson(), userHouseStatus);
    }

    public void RemoveHouse(House house)
    {
      if (houseMarkers.ContainsKey(house.Id))
      {
        houseMarkers[house.Id].Remove();
        houseMarkers.Remove(house.Id);
      }

      if (houseLabels.ContainsKey(house.Id))
      {
        houseLabels[house.Id].Remove();
        houseLabels.Remove(house.Id);
      }

      if (houseColShapes.ContainsKey(house.Id))
      {
        houseColShapes[house.Id].Remove();
        houseColShapes.Remove(house.Id);
      }

      if (houseBlips.ContainsKey(house.Id))
      {
        houseBlips[house.Id].Remove();
        houseBlips.Remove(house.Id);
      }

      foreach (IPlayer client in playerInColShape[house.Id])
      {
        client.TriggerEvent("CloseHouseMenu");
      }
    }

    public House GetNearHouse(Position position, DatabaseContext dbContext)
    {
      return dbContext.Houses
        .Include(h => h.Owner)
        .Include(h => h.BankAccount)
        .Include(h => h.Rentals)
        .ToList()
        .Where(h => h.Position.DistanceTo(position) < 5)
        .OrderBy(h => h.Position.DistanceTo(position))
        .FirstOrDefault();
    }

    public void ReloadAllHouses()
    {
      using var dbContext = GetDbContext();
      foreach (House house in dbContext.Houses.Include(h => h.Owner).Include(h => h.BankAccount))
      {
        RemoveHouse(house);
        LoadHouse(house);
      }
    }

    private void HouseManagerBuyHouseEvent(IPlayer player, params object[] args)
    {
      using var dbContext = GetDbContext();
      User user = player.GetUser(dbContext, bankAccount: true);

      if (user.HouseId != null)
      {
        player.SendMessage("Du kann nicht mehrere Häuser besitzen", ChatPrefix.Error);
        return;
      }

      House house = GetNearHouse(player.Position, dbContext);

      var userBank = user.BankAccount;

      if (userBank.Balance < house.Price)
      {
        player.SendMessage($"Du hast nicht genug Geld für das Haus ({house.Price.ToMoneyString()})", ChatPrefix.Error);
        return;
      }

      house.Owner = user;
      user.House = house;

      userBank.Balance -= house.Price;

      dbContext.SaveChanges();

      RemoveHouse(house);
      LoadHouse(house);
    }

    private void HouseManagerSetRentalFeeEvent(IPlayer player, params object[] args)
    {
      var rentalFee = args[0].ToInt();

      using var dbContext = GetDbContext();
      User user = player.GetUser(dbContext);

      if (user.HouseId == null)
      {
        player.SendMessage("Du besitzt kein Haus", ChatPrefix.Error);
        return;
      }

      House house = GetHouseById(user.HouseId.Value, dbContext);

      if (DateTime.Now - house.LastRentSetTime < TimeSpan.FromDays(7))
      {
        DateTime newPossibility = house.LastRentSetTime.AddDays(7);
        string dateStr = newPossibility.ToLongDateString();
        string timeStr = newPossibility.ToShortTimeString();
        player.SendMessage(
          $"Die Miete wurde in den letzten 7 Tagen schon verändert. Die Miete kann am ~y~{dateStr} um {timeStr}~s~ Uhr wieder geändert werden.", ChatPrefix.Info);
        return;
      }

      if (rentalFee < 0)
      {
        player.SendNotification("~r~Die Miete darf kein negativer Betrag sein!");
        return;
      }

      if (rentalFee > 5000)
      {
        player.SendNotification($"~r~Die Miete darf einen Preis von {5000.ToMoneyString()} nicht überschreiten!");
        return;
      }

      house.LastRentSetTime = DateTime.Now;
      house.RentalFee = rentalFee;

      dbContext.SaveChanges();

      player.SendNotification($"Der Mietpreis wurde auf ~g~{rentalFee.ToMoneyString()}~s~ gesetzt");

      RemoveHouse(house);
      LoadHouse(house);
    }

    private void HouseManagerCancelUserRentalEvent(IPlayer player, params object[] args)
    {
      string userName = args[0] as string;
      using var dbContext = GetDbContext();
      User user = player.GetUser(dbContext);
      if (user.HouseId == null)
      {
        player.SendMessage("Du besitzt kein Haus", ChatPrefix.Error);
        return;
      }

      User target = dbContext.Users.Where(u => u.Name == userName).FirstOrDefault();
      if (target == null)
      {
        player.SendNotification("~r~Dieser Spieler wurde nicht gefunden.");
        return;
      }

      House house = GetHouseById(user.HouseId.Value, dbContext);

      HouseRental rental = house.Rentals.Where(h => h.UserId == target.Id).FirstOrDefault();
      if (rental == null)
      {
        player.SendNotification("~r~Der Spieler ist nicht in deinem Haus eingemietet");
        return;
      }

      house.Rentals.Remove(rental);
      dbContext.SaveChanges();

      target.NewPlayer?.SendNotification($"~y~{player.Name}~s~ hat deinen Mietvertrag ~g~gekündigt~s~.");

      player.SendNotification("Du hast dem Spieler ~y~" + target.Name + "~s~ den Mietvertrag gekündigt.");
      SendPlayerHouseData(player, house);
    }

    private void HouseManagerRentInHouseEvent(IPlayer player, params object[] args)
    {
      using var dbContext = GetDbContext();
      User user = player.GetUser(dbContext);
      House house = GetNearHouse(player.Position, dbContext);

      if (house == null)
      {
        player.SendMessage("In deiner Nähe ist kein Haus", ChatPrefix.Error);
        return;
      }

      if (house.RentalFee == 0)
      {
        player.SendMessage("Dieses Haus hat keinen Platz für Mieter", ChatPrefix.Error);
        return;
      }

      HouseRental newRental = new HouseRental
      {
        HouseId = house.Id,
        UserId = user.Id
      };

      house.Rentals.Add(newRental);

      dbContext.SaveChanges();

      player.SendNotification("~g~Du hast dich in das Haus eingemietet");

      house.Owner?.NewPlayer?.SendNotification($"~y~{player.Name}~s~ hat sich in dein Haus eingemietet.");
      SendPlayerHouseData(player, house);
    }

    private void HouseManagerCancelOwnRentalEvent(IPlayer player, params object[] args)
    {
      using var dbContext = GetDbContext();
      User user = player.GetUser(dbContext);
      House house = GetNearHouse(player.Position, dbContext);

      if (house == null)
      {
        player.SendMessage("In deiner Nähe ist kein Haus", ChatPrefix.Error);
        return;
      }

      HouseRental rental = house.Rentals.Where(h => h.UserId == user.Id).FirstOrDefault();

      if (rental == null)
      {
        player.SendNotification("~r~Du bist nin diesem Haus nicht eingemietet");
        return;
      }

      house.Rentals.Remove(rental);

      dbContext.SaveChanges();

      player.SendNotification("~g~Du hast den Mietvertrag gekündigt.");
      house.Owner?.NewPlayer?.SendNotification($"~y~{player.Name}~s~ hat seinen Mietvertrag gekündigt.");

      RemoveHouse(house);
      LoadHouse(house);

      SendPlayerHouseData(player, house);
    }

    private void HouseManagerSellHouseEvent(IPlayer player, params object[] args)
    {
      using var dbContext = GetDbContext();
      User user = player.GetUser(dbContext, bankAccount: true);
      if (user.HouseId == null)
      {
        player.SendMessage("Du besitzt kein Haus", ChatPrefix.Error);
        return;
      }

      House house = GetHouseById(user.HouseId.Value, dbContext);
      house.OwnerId = null;
      user.HouseId = null;

      house.LastRentSetTime = DateTime.Now.Subtract(TimeSpan.FromDays(7));
      house.BankAccount.Balance = 0;

      var backMoney = (int)(house.Price * 0.4);

      player.SendMessage("Du bekommst vom Hausverkauf ~g~" + backMoney.ToMoneyString() + "~s~ zurück.");

      user.BankAccount.Balance += backMoney;
      dbContext.HouseRentals.RemoveRange(house.Rentals);

      dbContext.SaveChanges();

      player.SendMessage("!{#81F7BE}* Du hast dein Haus verkauft.");

      RemoveHouse(house);
      LoadHouse(house);

      SendPlayerHouseData(player, house);
    }

    private void HouseManagerWithdrawMoneyEvent(IPlayer player, object[] args)
    {
      var amount = args[0].ToInt();

      if (amount <= 0)
      {
        player.SendMessage("Du musst mindestens 1$ abheben", ChatPrefix.Error);
        return;
      }

      using var dbContext = GetDbContext();
      User user = player.GetUser(dbContext, bankAccount: true);
      if (user.HouseId == null)
      {
        player.SendMessage("Du besitzt kein Haus", ChatPrefix.Error);
        return;
      }

      House house = GetHouseById(user.HouseId.Value, dbContext);

      if (house.BankAccount.Balance < amount)
      {
        player.SendMessage("Auf dem Konto deines Hauses ist nicht genug Geld", ChatPrefix.Error);
        return;
      }

      user.BankAccount.Balance += amount;
      house.BankAccount.Balance -= amount;

      dbContext.SaveChanges();

      player.SendMessage($"Du hast {amount.ToMoneyString()} von dem Konto deines Hauses abgehoben", ChatPrefix.Info);

      SendPlayerHouseData(player, house);
    }
  }
}
