using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Linq.Expressions;
using MileageTraker.Web.Context;
using MileageTraker.Web.Models;
using MileageTraker.Web.Utility;
using MileageTraker.Web.ViewModels;
using MileageTraker.Web.ViewModels.Log;
using MileageTraker.Web.ViewModels.Vehicle;

namespace MileageTraker.Web.DAL
{
	public class DataService : IDisposable
	{
		private readonly MileageTrakerContext _db = new MileageTrakerContext();

		public void Dispose()
		{
			_db.Dispose();
		}

		#region Log

		public void AddLog(Log log)
		{
			log.Created = DateTime.Now;
			_db.Logs.Add(log);

			// update previous log links
			var previousLog = SearchPreviousLog(log);
			log.VehiclePreviousLog = previousLog;
			var nextLog = SearchNextLog(log);
			if (nextLog != null)
			{
				nextLog.VehiclePreviousLog = log;
				nextLog.User = _db.Users.Find(nextLog.User.UserId);
				_db.Entry(nextLog).State = EntityState.Modified;
			}

			_db.SaveChanges();

			UpdateCurrentOdometer(log.VehicleId);
		}

		public void UpdateLog(Log log)
		{
			// note: assumes log is already attached to current context

			// remove from the list
			var nextLog =
				(from l in _db.Logs
				 where l.VehiclePreviousLogId == log.LogId
				 select l).FirstOrDefault();
			if (nextLog != null)
			{
				nextLog.VehiclePreviousLogId = log.VehiclePreviousLogId;
				nextLog.User = _db.Users.Find(nextLog.User.UserId);
				_db.Entry(nextLog).State = EntityState.Modified;
			}

			log.VehiclePreviousLog = null;
			log.VehiclePreviousLogId = null;

			// add it back in new position
			var previousLog = SearchPreviousLog(log);
			if (previousLog != null)
				log.VehiclePreviousLogId = previousLog.LogId;
			var newNextLog = SearchNextLog(log);
			if (newNextLog != null)
			{
				newNextLog.VehiclePreviousLogId = log.LogId;
				newNextLog.User = _db.Users.Find(newNextLog.User.UserId);
				_db.Entry(newNextLog).State = EntityState.Modified;
			}

			log.User = _db.Users.Find(log.User.UserId);
			_db.Entry(log).State = EntityState.Modified;
			_db.SaveChanges();

			UpdateCurrentOdometer(log.VehicleId);
		}

		public void DeleteLog(int id)
		{
			var log = _db.Logs.Find(id);

			var nextLog = GetNextLog(log.LogId);
			if (nextLog != null)
			{
				nextLog.VehiclePreviousLog = log.VehiclePreviousLog;
				nextLog.User = _db.Users.Find(nextLog.User.UserId);
				_db.Entry(nextLog).State = EntityState.Modified;
			}

			_db.Logs.Remove(log);
			_db.SaveChanges();

			UpdateCurrentOdometer(log.VehicleId);
		}

		public IQueryable<Log> GetLogs()
		{
			return _db.Logs;
		}

		public Log GetLog(int id)
		{
			return _db.Logs.Find(id);
		}

		public Log GetNextLog(int id)
		{
			return
				(from l in _db.Logs
				 where l.VehiclePreviousLogId == id
				 select l).FirstOrDefault();
		}

		public IEnumerable<int> GetValidLogYears()
		{
			return
			from l in GetLogs()
			group l by new {year = l.Date.Year}
				into y
				orderby y.Key descending 
				select y.Key.year;
		}

		public IEnumerable<int> GetValidLogMonths(int year)
		{
			var start = new DateTime(year, 1, 1);
			var end = new DateTime(year + 1, 1, 1);
			return
			from l in GetLogs()
			where l.Date >= start && l.Date < end
			group l by new { month = l.Date.Month }
				into m
				orderby m.Key descending 
				select m.Key.month;
		}

		public IEnumerable<LogIndexViewModel> GetLogIndexViewModels(IQueryable<Log> logs)
		{
			var t =
				from log in logs
				select new { log, log.VehiclePreviousLog };
			return from l in t.ToList()
			       select new LogIndexViewModel(l.log, l.VehiclePreviousLog);
		}

		public IEnumerable<VehicleMileageItem> GetMonthlyVehicleMileageItems(LogQueryViewModel query)
		{
			var items =
				from l in
					from log in FilterLogs(_db.Logs, query)
					let previousLog = log.VehiclePreviousLog
					select new {log, previousLog, miles = (previousLog != null ? log.EndOdometer - previousLog.EndOdometer : 0)}
				group l by l.log.VehicleId
				into g
				join v in _db.Vehicles on g.Key equals v.VehicleId
				let miles = g.Sum(l => l.miles)
				let gasPurchased = g.Sum(l => l.log.GasPurchased)
				where miles > 0 || gasPurchased > 0
				let logPairs =
					from i in g
					orderby i.log.Date descending , i.log.EndOdometer descending
					select new { PreviousLog = i.previousLog, Log = i.log }
				select
					new
						{
							VehicleId = g.Key,
							GasPurchased = gasPurchased,
							Miles = miles,
							v.Prog,
							LogPairs = logPairs
						};

			return items.ToList().Select(i => new VehicleMileageItem
							{
								VehicleId = i.VehicleId,
								GasPurchased = i.GasPurchased,
								Miles = i.Miles,
								Prog = i.Prog,
								LogPairs = i.LogPairs.ToList()
									.Select(lp => new Tuple<Log,Log>(lp.PreviousLog, lp.Log))
							});
		}

		public IEnumerable<DriverMileageItem> GetMonthlyDriverMileageItems(LogQueryViewModel query)
		{
			var items =
				from l in
					from log in FilterLogs(_db.Logs, query)
					let previousLog = log.VehiclePreviousLog
					select new { log, previousLog, miles = (previousLog != null ? log.EndOdometer - previousLog.EndOdometer : 0) }
				group l by l.log.User into g
				let vehicleLogs =
					from i in g
					group i by i.log.VehicleId into v
					from vehicle in _db.Vehicles
					where vehicle.VehicleId == v.Key
					let vehicleProg = vehicle.Prog
					let vehicleMiles = v.Sum(l => l.miles)
					let vehicleGasPurchased = v.Sum(l => l.log.GasPurchased)
					where vehicleMiles > 0 || vehicleGasPurchased > 0
					let vehicleLogPairs =
						from i in v
						orderby i.log.Date descending, i.log.EndOdometer descending
						select new { PreviousLog = i.previousLog, Log = i.log }
					select new
						{
							VehicleId = v.Key,
							Prog = vehicleProg,
							Miles = vehicleMiles,
							GasPurchased = vehicleGasPurchased,
							LogPairs = vehicleLogPairs
						}
				select
					new
					{
						User = g.Key,
						VehicleLogs = vehicleLogs
					};

			return items.ToList().Select(i => new DriverMileageItem
			{
				DriverName = i.User.FullName,
				VehicleMileageItems = i.VehicleLogs.ToList().Select(v => new VehicleMileageItem
					{
						VehicleId = v.VehicleId,
						Prog = v.Prog,
						Miles = v.Miles,
						GasPurchased = v.GasPurchased,
						LogPairs = v.LogPairs.ToList()
							.Select(lp => new Tuple<Log, Log>(lp.PreviousLog, lp.Log))
					}).ToList()
			});
		}

		public static IQueryable<Log> FilterLogs(IQueryable<Log> logs, LogQueryViewModel query)
		{
			if (query.Year.HasValue & query.Month.HasValue)
			{
				var start = new DateTime(query.Year.Value, query.Month.Value, 1);
				var end = start.AddMonths(1);
				logs = logs.Where(l => l.Date >= start && l.Date < end);
			}

			if (query.LogType.HasValue)
			{
				// WTF The specified value is not an instance of type 'Edm.Int32'
				Expression<Func<Log, bool>> predicate;
				switch (query.LogType.Value)
				{
					case MileageLogType.NonCommuting:
						 predicate = l => l.LogType.Value == 1;
						break;
					case MileageLogType.Commuting:
						predicate = l => l.LogType.Value == 2;
						break;
					case MileageLogType.GasPurchase:
						predicate = l => l.LogType.Value == 3;
						break;
					default:
						throw new ArgumentOutOfRangeException();
				}
				logs = logs.Where(predicate);
			}

			return logs;
		}

		public IEnumerable<Log> GetRecentLogsByUsername(string username)
		{
			return
				(from l in _db.Logs
				 where l.User.Username == username
				 orderby l.Created descending
				 select l)
					.Take(3)
					.ToList();
		}

		public Log SearchPreviousLog(Log current)
		{
			var vehicleId = current.VehicleId;
			var date = current.Date;
			var endOdometer = current.EndOdometer;
			return SearchPreviousLog(endOdometer, vehicleId, date, current.Created, current.LogId);
		}

		public Log SearchPreviousLog(int endOdometer, string vehicleId, DateTime date, DateTime created, int? excludeLogId = null)
		{
			return (from pl in _db.Logs
			        where pl.VehicleId == vehicleId &&
			              ((pl.EndOdometer < endOdometer && pl.Date <= date)
						   || (pl.EndOdometer == endOdometer && pl.Date < date)
						   || (pl.EndOdometer == endOdometer && pl.Date == date && pl.Created < created))
			        orderby pl.EndOdometer descending, pl.Date descending, pl.Created descending
			        select pl)
					.FirstOrDefault(l => !excludeLogId.HasValue || l.LogId != excludeLogId.Value);
		}

		public Log SearchNextLog(Log current)
		{
			var vehicleId = current.VehicleId;
			var date = current.Date;
			var endOdometer = current.EndOdometer;
			return SearchNextLog(endOdometer, vehicleId, date, current.Created, current.LogId);
		}

		public Log SearchNextLog(int endOdometer, string vehicleId, DateTime date, DateTime created, int? excludeLogId = null)
		{
			return (from nl in GetLogs()
			        where nl.VehicleId == vehicleId &&
			              ((nl.EndOdometer > endOdometer && nl.Date >= date)
						   || (nl.EndOdometer == endOdometer && nl.Date > date)
						   || (nl.EndOdometer == endOdometer && nl.Date >= date && nl.Created > created)) 
			        orderby nl.EndOdometer ascending, nl.Date ascending, nl.Created ascending 
			        select nl)
					.FirstOrDefault(l => !excludeLogId.HasValue || l.LogId != excludeLogId.Value);
		}

		public IEnumerable<PurposeType> GetPurposeTypes()
		{
			return
				(from pt in _db.PurposeTypes
				 select pt).OrderBy(pt => pt.SortOrder).ToList();
		}

		public void AddPurposeType(PurposeType purposeType)
		{
			_db.PurposeTypes.Add(purposeType);
			_db.SaveChanges();
		}

		public void UpdatePurposeType(PurposeType purposeType)
		{
			_db.Entry(purposeType).State = EntityState.Modified;
			_db.SaveChanges();
		}

		public PurposeType FindPurposeType(int id)
		{
			return GetPurposeTypes().FirstOrDefault(pt => pt.PurposeTypeId == id);
		}

		#endregion

		#region Vehicle

		public void AddVehicle(Vehicle vehicle)
		{
			if (vehicle.Assigned == "Unassigned")
				vehicle.Assigned = null;
			_db.Vehicles.Add(vehicle);
			_db.SaveChanges();
		}

		public void UpdateVehicle(Vehicle vehicle)
		{
			if (vehicle.Assigned == "Unassigned")
				vehicle.Assigned = null;
			_db.Entry(vehicle).State = EntityState.Modified;
			_db.SaveChanges();
		}

		public IEnumerable<Vehicle> GetVehicles()
		{
			var vehicles = _db.Vehicles;
			return vehicles;
		}

		public Vehicle GetVehicle(string id)
		{
			return _db.Vehicles.Find(id);
		}

		public void UpdateCurrentOdometer(string vehicleId)
		{
			var mostRecentOdometerQuery =
				from log in _db.Logs
				where
					log.VehicleId == vehicleId
				orderby log.Date descending, log.EndOdometer descending
				select log;
			var mostRecentOdometer = mostRecentOdometerQuery.FirstOrDefault();
			var vehicle = _db.Vehicles.Find(vehicleId);
			if (mostRecentOdometer != null && mostRecentOdometer.EndOdometer != vehicle.CurrentOdometer)
			{
				vehicle.CurrentOdometer = mostRecentOdometer.EndOdometer;
				UpdateVehicle(vehicle);
			}
		}

		public void ValidateOdometerChronology(string vehicleId, int odometer, DateTime date)
		{
			var conflictingLog = GetFirstConflictingOdometerChronologyLog(vehicleId, date, odometer);
			if (conflictingLog != null)
			{
				throw new ChronologicalOrderException(
					String.Format(
						"Odometer of {0} miles on {1:d} conflicts with other logs for vehicle {2}" +
						", including an odometer log of {3} miles on {4:d} reported by {5}.",
						odometer, date, vehicleId, conflictingLog.EndOdometer, conflictingLog.Date, conflictingLog.User.FullName));
			}
		}

		public Log GetFirstConflictingOdometerChronologyLog(string vehicleId, DateTime date, int odometer)
		{
			var conflictingLogsQuery =
				from log in _db.Logs
				let existingDate = log.Date
				let existingOdometer = log.EndOdometer
				where
					log.VehicleId == vehicleId &&
					!(existingDate == date
					  || existingOdometer == odometer
					  || existingDate < date && existingOdometer < odometer
					  || existingDate > date && existingOdometer > odometer)
				orderby existingOdometer descending 
				select log;

			return conflictingLogsQuery.FirstOrDefault();
		}

		#endregion

		#region City

		public string GetCitiesCorrected(string term)
		{
			var termLower = term.ToLower();
			return (from city in GetCities().ToList()
				   let cityToLower = city.Name.ToLower()
				   let similarity = Algorithms.Similarity(cityToLower, termLower)
					where city.Name.StartsWith(term) || Algorithms.Similarity(cityToLower, termLower) > .8
			       select city.Name).FirstOrDefault();
		}

		public IEnumerable<string> GetCitiesAutocomplete(string term)
		{
			return 
				from city in GetCities()
				   where city.Name.StartsWith(term)
				   select city.Name;
		}

		private IQueryable<City> GetCities()
		{
			return _db.Cities;
		}

		#endregion

		#region Membership

		public void AddUser(User user)
		{
			user.Created = DateTime.Now;

			_db.Users.Add(user);

			_db.SaveChanges();
		}

		public void UpdateUser(User user)
		{
			_db.Entry(user).State = EntityState.Modified;
			_db.SaveChanges();
		}

		/// <summary>
		/// Update only fields not needing extra membership provider support
		/// </summary>
		public void UpdateUserPersonalInfo(User user)
		{
			var original = GetUser(user.UserId);

			original.Comment = user.Comment;
			original.Email = user.Email;
			original.FullName = user.FullName;
			original.Username = user.Username;
			
			UpdateUser(original);
		}

		public void UpdateUserPassword(Guid userId, string password)
		{
			var user = GetUser(userId);
			user.Password = Crypto.HashPassword(password);
			user.LastPasswordChangedDate = DateTime.Now;
			user.IsLockedOut = false;
			user.PasswordFailuresSinceLastSuccess = 0;
			user.PasswordResetToken = null;
			UpdateUser(user);
		}

		public void UpdateUserLastActivity(string username)
		{
			var user = FindUserByUsername(username);
			if (user != null)
			{
				user.LastActivityDate = DateTime.Now;
				UpdateUser(user);
			}
		}

		public IQueryable<User> GetUsers()
		{
			return _db.Users;
		}

		public User GetUser(Guid guid)
		{
			return _db.Users.Find(guid);
		}

		public User FindUserByUsername(string username)
		{
			return _db.Users.FirstOrDefault(u => username.Equals(u.Username, StringComparison.InvariantCultureIgnoreCase));
		}

		public User FindUserByFullName(string fullName)
		{
			return _db.Users.FirstOrDefault(u => fullName.Equals(u.FullName, StringComparison.InvariantCultureIgnoreCase));
		}

		public User FindUserByEmail(string  email)
		{
			return _db.Users.FirstOrDefault(u => email.Equals(u.Email, StringComparison.InvariantCultureIgnoreCase));
		}

		public IQueryable<Role> GetRoles()
		{
			return _db.Roles;
		}

		public IEnumerable<string> GetUserFullNamesAutocomplete(string term)
		{
			var termLower = term.ToLower();
			return from name in GetUserFullNames()
				   let nameLower = name.ToLower()
				   where nameLower.StartsWith(termLower) || Algorithms.Similarity(nameLower, termLower) > .8
				   select name;
		}

		public IEnumerable<string> GetUserFullNames()
		{
			var logNames =
				(from l in _db.Users
				 select l.FullName);
			return logNames;
		}

		#endregion
	}
}