namespace MyOffice.DbContext;

using Data.Models.Accounts;
using Data.Models.Currencies;
using Data.Models.Items;
using Data.Models.Users;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

/// <summary>
/// Fills an empty database with three demo users (UAH/USD/EUR) and sample catalog data.
/// </summary>
public static class DemoDataSeeder
{
	private static readonly PasswordHasher<object> PasswordHasher = new();

	private static readonly string[] CurrencyCodes = ["UAH", "USD", "EUR"];

	private static readonly (string Code, string Email, string Password, string FirstName)[] Users =
	[
		("UAH", "user_UAH@user_UAH.userUAH", "user_UAH", "User UAH"),
		("USD", "user_USD@user_USD.userUSD", "user_USD", "User USD"),
		("EUR", "user_EUR@user_EUR.userEUR", "user_EUR", "User EUR"),
	];

	/// <summary>1 USD = 42 UAH, 1 EUR = 50 UAH.</summary>
	private const decimal UsdInUah = 42m;
	private const decimal EurInUah = 50m;

	public static async Task SeedIfEmptyAsync(AppDbContext db, CancellationToken cancellationToken = default)
	{
		if (await db.Users.AnyAsync(cancellationToken))
		{
			return;
		}

		// AppDbContext defaults to NoTracking; seeding needs identity values and graph inserts.
		var previousTracking = db.ChangeTracker.QueryTrackingBehavior;
		var previousDetect = db.ChangeTracker.AutoDetectChangesEnabled;
		db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.TrackAll;
		db.ChangeTracker.AutoDetectChangesEnabled = true;

		try
		{
			var yearStart = new DateTime(DateTime.UtcNow.Year, 1, 1, 0, 0, 0, DateTimeKind.Utc);
			var rng = new Random(2026);

			foreach (var (code, email, password, firstName) in Users)
			{
				await SeedUserAsync(db, code, email, password, firstName, yearStart, rng, cancellationToken);
			}

			await db.SaveChangesAsync(cancellationToken);
		}
		finally
		{
			db.ChangeTracker.QueryTrackingBehavior = previousTracking;
			db.ChangeTracker.AutoDetectChangesEnabled = previousDetect;
			db.ChangeTracker.Clear();
		}
	}

	private static async Task SeedUserAsync(
		AppDbContext db,
		string homeCurrency,
		string email,
		string password,
		string firstName,
		DateTime rateDate,
		Random rng,
		CancellationToken cancellationToken)
	{
		var ua = homeCurrency == "UAH";
		var userId = Guid.NewGuid();

		var user = new User
		{
			Id = userId,
			UserName = email,
			Email = email,
			PasswordHash = PasswordHasher.HashPassword(new object(), password),
			FirstName = firstName,
			FullName = firstName,
			IsEmailConfirmed = true,
			CurrencyId = homeCurrency,
		};
		db.Users.Add(user);

		var currencies = new Dictionary<string, Currency>(StringComparer.Ordinal);
		foreach (var code in CurrencyCodes)
		{
			var currency = new Currency
			{
				Id = Guid.NewGuid(),
				UserId = userId,
				CurrencyGlobalId = code,
				Name = CurrencyDisplayName(code, ua),
				ShortName = code,
				IsPrimary = code == homeCurrency,
			};
			db.Currencies.Add(currency);
			db.CurrencyRates.Add(new CurrencyRate
			{
				CurrencyId = currency.Id,
				DateTime = rateDate,
				Quantity = 1,
				Rate = RateInHome(code, homeCurrency),
			});
			currencies[code] = currency;
		}

		// UnCategorized convention: category Id == user Id
		db.ItemCategories.Add(new ItemCategory
		{
			Id = userId,
			UserId = userId,
			Name = ua ? "Без категорії" : "UnCategorized",
			Items = [],
			IsInternal = true,
		});

		var accountCategoryDefs = ua
			? new (string Key, string Name)[] { ("CASH", "Готівка"), ("BANK", "Банк"), ("DEPOSIT", "Депозит") }
			: [("CASH", "CASH"), ("BANK", "BANK"), ("DEPOSIT", "DEPOSIT")];

		var accounts = new List<Account>();
		foreach (var (key, name) in accountCategoryDefs)
		{
			var category = new AccountCategory
			{
				Id = Guid.NewGuid(),
				UserId = userId,
				Name = name,
			};
			db.AccountCategories.Add(category);

			foreach (var code in CurrencyCodes)
			{
				var account = new Account
				{
					Id = Guid.NewGuid(),
					OwnerId = userId,
					CurrencyGlobalId = code,
					Name = $"{name} {code}",
				};
				db.Accounts.Add(account);
				db.AccountAccountCategories.Add(new AccountAccountCategory
				{
					AccountId = account.Id,
					CategoryId = category.Id,
				});
				db.AccountAccesses.Add(new AccountAccess
				{
					AccountId = account.Id,
					UserId = userId,
					OwnerId = userId,
					IsAllowRead = true,
					IsAllowWrite = true,
					IsAllowManage = true,
					Type = AccountAccessTypeEnum.balance,
				});
				accounts.Add(account);
			}
		}

		var incomeCategory = new ItemCategory
		{
			Id = Guid.NewGuid(),
			UserId = userId,
			Name = ua ? "Доходи" : "INCOME",
			Items = [],
		};
		var outcomeCategory = new ItemCategory
		{
			Id = Guid.NewGuid(),
			UserId = userId,
			Name = ua ? "Витрати" : "OUTCOME",
			Items = [],
		};
		db.ItemCategories.AddRange(incomeCategory, outcomeCategory);

		var incomeNames = ua
			? new[] { "Готівка", "Офіс" }
			: ["CASH", "OFFICE"];
		var outcomeNames = ua
			? new[]
			{
				"Товари", "Бензин", "Steam", "Продукти", "Кафе",
				"Оренда", "Комунальні", "Інтернет", "Зв'язок", "Розваги",
			}
			: [
				"Goods", "Petrol", "Steam", "Groceries", "Cafe",
				"Rent", "Utilities", "Internet", "Mobile", "Entertainment",
			];

		var incomeItems = await AddItemsAsync(db, incomeCategory.Id, incomeNames, cancellationToken);
		var outcomeItems = await AddItemsAsync(db, outcomeCategory.Id, outcomeNames, cancellationToken);

		await db.SaveChangesAsync(cancellationToken);

		SeedMotions(db, accounts, incomeItems, outcomeItems, rng, count: 100);
	}

	private static async Task<List<Item>> AddItemsAsync(
		AppDbContext db,
		Guid categoryId,
		IEnumerable<string> names,
		CancellationToken cancellationToken)
	{
		var items = new List<Item>();
		foreach (var name in names)
		{
			var global = db.ItemGlobals.Local.FirstOrDefault(x => x.Name == name)
				?? await db.ItemGlobals.FirstOrDefaultAsync(x => x.Name == name, cancellationToken);
			if (global == null)
			{
				global = new ItemGlobal { Id = Guid.NewGuid(), Name = name };
				db.ItemGlobals.Add(global);
			}

			var item = new Item
			{
				CategoryId = categoryId,
				ItemGlobalId = global.Id,
			};
			db.Items.Add(item);
			items.Add(item);
		}

		return items;
	}

	private static void SeedMotions(
		AppDbContext db,
		List<Account> accounts,
		List<Item> incomeItems,
		List<Item> outcomeItems,
		Random rng,
		int count)
	{
		var now = DateTime.UtcNow;
		var from = now.AddMonths(-3);

		for (var i = 0; i < count; i++)
		{
			var isIncome = rng.Next(2) == 0;
			var item = isIncome
				? incomeItems[rng.Next(incomeItems.Count)]
				: outcomeItems[rng.Next(outcomeItems.Count)];
			var account = accounts[rng.Next(accounts.Count)];
			var days = (now - from).TotalDays;
			var when = from.AddDays(rng.NextDouble() * days);
			var amount = Math.Round((decimal)(rng.NextDouble() * (isIncome ? 4500 : 700) + (isIncome ? 200 : 20)), 2);

			db.Motions.Add(new Motion
			{
				Id = Guid.NewGuid(),
				AccountId = account.Id,
				ItemId = item.Id,
				DateTime = when,
				CreatedOn = when,
				Description = isIncome ? "Demo income" : "Demo expense",
				AmountPlus = isIncome ? amount : 0,
				AmountMinus = isIncome ? 0 : amount,
			});
		}
	}

	private static decimal RateInHome(string currencyCode, string homeCurrency)
	{
		static decimal InUah(string code) => code switch
		{
			"UAH" => 1m,
			"USD" => UsdInUah,
			"EUR" => EurInUah,
			_ => 1m,
		};

		return InUah(currencyCode) / InUah(homeCurrency);
	}

	private static string CurrencyDisplayName(string code, bool ua) => (code, ua) switch
	{
		("UAH", true) => "Гривня",
		("USD", true) => "Долар США",
		("EUR", true) => "Євро",
		("UAH", _) => "Ukrainian hryvnia",
		("USD", _) => "US Dollar",
		("EUR", _) => "Euro",
		_ => code,
	};
}
