using AfRApay.Web.Backend.Database;
using AfRApay.Web.Backend.Database.Tables;
using AfRApay.Web.Controllers.Schema;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Swashbuckle.AspNetCore.Filters;

namespace AfRApay.Web.Controllers;

[ApiController]
public class CardController : Controller {
	/// <summary>
	/// Links the given card to the user currently in link mode.
	/// </summary>
	/// <param name="card">The ID of the card</param>
	/// <param name="reader">Type of reader that scanned the card</param>
	/// <response code="200">Returns 200 if the link succeeded</response>
	/// <response code="304">Returns 304 if the card was already linked</response>
	/// <response code="404">Returns 404 if no link process is active</response>
	[HttpPut]
	[Produces("application/json")]
	[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UserResponse))]
	[ProducesResponseType(StatusCodes.Status304NotModified)]
	[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
	[SwaggerResponseExample(StatusCodes.Status200OK, typeof(UserUpdatedExample))]
	[SwaggerResponseExample(StatusCodes.Status404NotFound, typeof(ErrorNoActiveLinkProcessExample))]
	[Route("/api/card/{card}/link")]
	public async Task<IActionResult> Link(string card, [FromQuery] string? reader) {
		var db = new DatabaseContext();
		if (db.Cards.Any(p => p.Id == card)) {
			return StatusCode(StatusCodes.Status304NotModified);
		}

		var linkFlag = db.Config.FirstOrDefault(p => p.Name == "link");

		if (string.IsNullOrWhiteSpace(linkFlag?.Value)) {
			return NotFound(new ErrorResponse("No active link process"));
		}

		var lTimeFlag = db.Config.FirstOrDefault(p => p.Name == "lTime");
		if (string.IsNullOrWhiteSpace(lTimeFlag?.Value)) {
			return NotFound(new ErrorResponse("No active link process"));
		}

		if (DateTime.UtcNow - DateTime.Parse(lTimeFlag.Value) > TimeSpan.FromMinutes(5)) {
			return NotFound(new ErrorResponse("No active link process"));
		}

		var user = db.Users.FirstOrDefault(p => p.Id == int.Parse(linkFlag.Value));
		
		if (user == null) {
			return NotFound(new ErrorResponse("No active link process"));
		}
		
		linkFlag.Value = "";

		var type = GetCardType(reader, card);

		db.Add(new Card { Id = card, User = user, Type = type });
		await db.SaveChangesAsync();
		return Ok(new UserResponse(user));
	}

	/// <summary>
	/// Creates a transaction that changes the balance of the the user the card is linked to by the specified amount.
	/// </summary>
	/// <param name="card">The ID of the card (alternatively, magic card identifier @username)</param>
	/// <param name="ik">Random string (idempotency key) which is consistent across request retries</param>
	/// <param name="amount">Positive or negative number of cents representing the relative change in balance</param>
	/// <param name="reader">Type of reader that scanned the card</param>
	/// <response code="200">Returns 200 if the transaction succeeded</response>
	/// <response code="404">Returns 404 if the card isn't linked to any account</response>
	/// <response code="412">Returns 412 if the transaction failed because the balance would be out of range after the transaction</response>
	[HttpPut]
	[Produces("application/json")]
	[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UserResponse))]
	[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
	[ProducesResponseType(StatusCodes.Status412PreconditionFailed, Type = typeof(ErrorResponse))]
	[SwaggerResponseExample(200, typeof(UserUpdatedExample))]
	[SwaggerResponseExample(404, typeof(ErrorUnknownCardExample))]
	[SwaggerResponseExample(412, typeof(ErrorBalanceOutOfRangeExample))]
	[Route("/api/card/{card}/transaction/{ik}")]
	public async Task<IActionResult> Transaction(string card, string ik, [FromQuery] int amount, [FromQuery] string? reader) {
		var db = new DatabaseContext();
		if (db.Cards.Any(p => p.Id == card)) {
			var user = db.Cards.Include(p => p.User).First(p => p.Id == card).User;

			if (ik == "" || ik != user.LastIdempotencyKey) {
				user.LastIdempotencyKey = ik;
				switch (user.Balance + amount) {
					case < -9999: return StatusCode(412, new ErrorResponse("Balance too low!"));
					case > 99999: return StatusCode(412, new ErrorResponse("Balance too high!"));
				}

				user.Balance += amount;
			}

			var dbCard  = db.Cards.First(p => p.Id == card);
			var newType = GetCardType(reader, card);

			if (dbCard.Type != newType)
				dbCard.Type = newType;

			await db.SaveChangesAsync();
			return Ok(new UserResponse(user));
		}
		
		if (db.Users.Any(p => card.Length > 1 && p.Nickname == card.Substring(1))) {
			var user = db.Users.First(p => card.Length > 1 && p.Nickname == card.Substring(1));
			if (ik == "" || ik != user.LastIdempotencyKey) {
				user.LastIdempotencyKey = ik;
				switch (user.Balance + amount) {
					case < -9999: return StatusCode(412, new ErrorResponse("Balance too low!"));
					case > 99999: return StatusCode(412, new ErrorResponse("Balance too high!"));
				}

				user.Balance += amount;
			}

			await db.SaveChangesAsync();
			return Ok(new UserResponse(user));
		}

		return NotFound(new ErrorResponse("Unknown card."));
	}

	/// <summary>
	/// Returns the balance of the the user the card is linked to.
	/// </summary>
	/// <param name="card">The ID of the card (alternatively, magic card identifier @username)</param>
	/// <param name="reader">Type of reader that scanned the card</param>
	/// <response code="200">Returns 200 if the request succeeded</response>
	/// <response code="404">Returns 404 if the card isn't linked to any account</response>
	[HttpGet]
	[Produces("application/json")]
	[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UserResponse))]
	[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
	[SwaggerResponseExample(200, typeof(UserUpdatedExample))]
	[SwaggerResponseExample(404, typeof(ErrorUnknownCardExample))]
	[Route("/api/card/{card}/balance")]
	public async Task<IActionResult> Balance(string card, [FromQuery] string? reader) {
		var db = new DatabaseContext();
		if (db.Cards.Any(p => p.Id == card)) {
			var user = db.Cards.Include(p => p.User).First(p => p.Id == card).User;

			var dbCard  = db.Cards.First(p => p.Id == card);
			var newType = GetCardType(reader, card);

			if (dbCard.Type != newType) {
				dbCard.Type = newType;
				await db.SaveChangesAsync();
			}

			return Ok(new UserResponse(user));
		}

		if (db.Users.Any(p => card.Length > 1 && p.Nickname == card.Substring(1))) {
			var user = db.Users.First(p => card.Length > 1 && p.Nickname == card.Substring(1));
			return Ok(new UserResponse(user));
		}

		return NotFound(new ErrorResponse("Unknown card."));
	}

	private class UserUpdatedExample : IExamplesProvider<UserResponse> {
		public UserResponse GetExamples() => new(new User { Id = 123, Nickname = "testman", Balance = 2550, LastIdempotencyKey = "5a6c94aa" });
	}

	private class ErrorUnknownCardExample : IExamplesProvider<ErrorResponse> {
		public ErrorResponse GetExamples() => new("Unknown card");
	}

	private class ErrorBalanceOutOfRangeExample : IExamplesProvider<ErrorResponse> {
		public ErrorResponse GetExamples() => new("Balance out of range");
	}

	private class ErrorNoActiveLinkProcessExample : IExamplesProvider<ErrorResponse> {
		public ErrorResponse GetExamples() => new("No active link process");
	}

	private static Card.CardType GetCardType(string? reader, string cardNumber) => reader switch {
		// these seem not be reliable
		//"pn532-iso14443a" when cardNumber.Length == 8                                                                  => Card.CardType.NfcMifareClassic,
		//"pn532-iso14443a" when cardNumber.Length == 12 && cardNumber.EndsWith("0218")                                  => Card.CardType.NfcOvChipkaart,
		//"pn532-iso14443a" when cardNumber.Length == 14 && cardNumber.StartsWith("04") && cardNumber.EndsWith("80")     => Card.CardType.NfcItso,
		//"pn532-iso14443a" when [???]                                                                                   => Card.CardType.NfcContactless,

		"pn532-iso14443a" when cardNumber.Length == 14 && cardNumber.EndsWith("4e80") => Card.CardType.NfcTflOyster,
		"pn532-iso14443a" when cardNumber.Length == 14 && cardNumber.EndsWith("5e80") => Card.CardType.NfcTflOyster,
		"pn532-iso14443a" when cardNumber.Length == 14 && cardNumber.EndsWith("5f80") => Card.CardType.NfcTflOyster,
		"pn532-iso14443a"                                                             => Card.CardType.NfcGeneric,
		"pn532-felica" when cardNumber.Length == 16 && cardNumber.StartsWith("0112")  => Card.CardType.FeliCaSuica,
		"pn532-felica" when cardNumber.Length == 16 && cardNumber.StartsWith("0101")  => Card.CardType.FeliCaPasmo,
		"pn532-felica" when cardNumber.Length == 16 && cardNumber.StartsWith("0139")  => Card.CardType.FeliCaApplePay,
		"pn532-felica"                                                                => Card.CardType.FeliCaGeneric,
		"rdm6300"                                                                     => Card.CardType.Rfid125KhzGeneric,
		_                                                                             => Card.CardType.Unknown
	};
}
