using System.Text;
using System.Text.Json;
using Bit.Admin.Models;
using Bit.Core.Entities;
using Bit.Core.Models.BitStripe;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Bit.Admin.Controllers;

[Authorize]
[SelfHosted(NotSelfHostedOnly = true)]
public class ToolsController : Controller
{
    private readonly GlobalSettings _globalSettings;
    private readonly IOrganizationRepository _organizationRepository;
    private readonly IOrganizationService _organizationService;
    private readonly IUserService _userService;
    private readonly ITransactionRepository _transactionRepository;
    private readonly IInstallationRepository _installationRepository;
    private readonly IOrganizationUserRepository _organizationUserRepository;
    private readonly IPaymentService _paymentService;
    private readonly ITaxRateRepository _taxRateRepository;
    private readonly IStripeAdapter _stripeAdapter;

    public ToolsController(
        GlobalSettings globalSettings,
        IOrganizationRepository organizationRepository,
        IOrganizationService organizationService,
        IUserService userService,
        ITransactionRepository transactionRepository,
        IInstallationRepository installationRepository,
        IOrganizationUserRepository organizationUserRepository,
        ITaxRateRepository taxRateRepository,
        IPaymentService paymentService,
        IStripeAdapter stripeAdapter)
    {
        _globalSettings = globalSettings;
        _organizationRepository = organizationRepository;
        _organizationService = organizationService;
        _userService = userService;
        _transactionRepository = transactionRepository;
        _installationRepository = installationRepository;
        _organizationUserRepository = organizationUserRepository;
        _taxRateRepository = taxRateRepository;
        _paymentService = paymentService;
        _stripeAdapter = stripeAdapter;
    }

    public IActionResult ChargeBraintree()
    {
        return View(new ChargeBraintreeModel());
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> ChargeBraintree(ChargeBraintreeModel model)
    {
        if (!ModelState.IsValid)
        {
            return View(model);
        }

        var btGateway = new Braintree.BraintreeGateway
        {
            Environment = _globalSettings.Braintree.Production ?
                Braintree.Environment.PRODUCTION : Braintree.Environment.SANDBOX,
            MerchantId = _globalSettings.Braintree.MerchantId,
            PublicKey = _globalSettings.Braintree.PublicKey,
            PrivateKey = _globalSettings.Braintree.PrivateKey
        };

        var btObjIdField = model.Id[0] == 'o' ? "organization_id" : "user_id";
        var btObjId = new Guid(model.Id.Substring(1, 32));

        var transactionResult = await btGateway.Transaction.SaleAsync(
            new Braintree.TransactionRequest
            {
                Amount = model.Amount.Value,
                CustomerId = model.Id,
                Options = new Braintree.TransactionOptionsRequest
                {
                    SubmitForSettlement = true,
                    PayPal = new Braintree.TransactionOptionsPayPalRequest
                    {
                        CustomField = $"{btObjIdField}:{btObjId}"
                    }
                },
                CustomFields = new Dictionary<string, string>
                {
                    [btObjIdField] = btObjId.ToString()
                }
            });

        if (!transactionResult.IsSuccess())
        {
            ModelState.AddModelError(string.Empty, "Charge failed. " +
                "Refer to Braintree admin portal for more information.");
        }
        else
        {
            model.TransactionId = transactionResult.Target.Id;
            model.PayPalTransactionId = transactionResult.Target?.PayPalDetails?.CaptureId;
        }
        return View(model);
    }

    public IActionResult CreateTransaction(Guid? organizationId = null, Guid? userId = null)
    {
        return View("CreateUpdateTransaction", new CreateUpdateTransactionModel
        {
            OrganizationId = organizationId,
            UserId = userId
        });
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> CreateTransaction(CreateUpdateTransactionModel model)
    {
        if (!ModelState.IsValid)
        {
            return View("CreateUpdateTransaction", model);
        }

        await _transactionRepository.CreateAsync(model.ToTransaction());
        if (model.UserId.HasValue)
        {
            return RedirectToAction("Edit", "Users", new { id = model.UserId });
        }
        else
        {
            return RedirectToAction("Edit", "Organizations", new { id = model.OrganizationId });
        }
    }

    public async Task<IActionResult> EditTransaction(Guid id)
    {
        var transaction = await _transactionRepository.GetByIdAsync(id);
        if (transaction == null)
        {
            return RedirectToAction("Index", "Home");
        }
        return View("CreateUpdateTransaction", new CreateUpdateTransactionModel(transaction));
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> EditTransaction(Guid id, CreateUpdateTransactionModel model)
    {
        if (!ModelState.IsValid)
        {
            return View("CreateUpdateTransaction", model);
        }
        await _transactionRepository.ReplaceAsync(model.ToTransaction(id));
        if (model.UserId.HasValue)
        {
            return RedirectToAction("Edit", "Users", new { id = model.UserId });
        }
        else
        {
            return RedirectToAction("Edit", "Organizations", new { id = model.OrganizationId });
        }
    }

    public IActionResult PromoteAdmin()
    {
        return View();
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> PromoteAdmin(PromoteAdminModel model)
    {
        if (!ModelState.IsValid)
        {
            return View(model);
        }

        var orgUsers = await _organizationUserRepository.GetManyByOrganizationAsync(
            model.OrganizationId.Value, null);
        var user = orgUsers.FirstOrDefault(u => u.UserId == model.UserId.Value);
        if (user == null)
        {
            ModelState.AddModelError(nameof(model.UserId), "User Id not found in this organization.");
        }
        else if (user.Type != Core.Enums.OrganizationUserType.Admin)
        {
            ModelState.AddModelError(nameof(model.UserId), "User is not an admin of this organization.");
        }

        if (!ModelState.IsValid)
        {
            return View(model);
        }

        user.Type = Core.Enums.OrganizationUserType.Owner;
        await _organizationUserRepository.ReplaceAsync(user);
        return RedirectToAction("Edit", "Organizations", new { id = model.OrganizationId.Value });
    }

    public IActionResult GenerateLicense()
    {
        return View();
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> GenerateLicense(LicenseModel model)
    {
        if (!ModelState.IsValid)
        {
            return View(model);
        }

        User user = null;
        Organization organization = null;
        if (model.UserId.HasValue)
        {
            user = await _userService.GetUserByIdAsync(model.UserId.Value);
            if (user == null)
            {
                ModelState.AddModelError(nameof(model.UserId), "User Id not found.");
            }
        }
        else if (model.OrganizationId.HasValue)
        {
            organization = await _organizationRepository.GetByIdAsync(model.OrganizationId.Value);
            if (organization == null)
            {
                ModelState.AddModelError(nameof(model.OrganizationId), "Organization not found.");
            }
            else if (!organization.Enabled)
            {
                ModelState.AddModelError(nameof(model.OrganizationId), "Organization is disabled.");
            }
        }
        if (model.InstallationId.HasValue)
        {
            var installation = await _installationRepository.GetByIdAsync(model.InstallationId.Value);
            if (installation == null)
            {
                ModelState.AddModelError(nameof(model.InstallationId), "Installation not found.");
            }
            else if (!installation.Enabled)
            {
                ModelState.AddModelError(nameof(model.OrganizationId), "Installation is disabled.");
            }
        }

        if (!ModelState.IsValid)
        {
            return View(model);
        }

        if (organization != null)
        {
            var license = await _organizationService.GenerateLicenseAsync(organization,
                model.InstallationId.Value, model.Version);
            var ms = new MemoryStream();
            await JsonSerializer.SerializeAsync(ms, license, JsonHelpers.Indented);
            ms.Seek(0, SeekOrigin.Begin);
            return File(ms, "text/plain", "bitwarden_organization_license.json");
        }
        else if (user != null)
        {
            var license = await _userService.GenerateLicenseAsync(user, null, model.Version);
            var ms = new MemoryStream();
            ms.Seek(0, SeekOrigin.Begin);
            await JsonSerializer.SerializeAsync(ms, license, JsonHelpers.Indented);
            ms.Seek(0, SeekOrigin.Begin);
            return File(ms, "text/plain", "bitwarden_premium_license.json");
        }
        else
        {
            throw new Exception("No license to generate.");
        }
    }

    public async Task<IActionResult> TaxRate(int page = 1, int count = 25)
    {
        if (page < 1)
        {
            page = 1;
        }

        if (count < 1)
        {
            count = 1;
        }

        var skip = (page - 1) * count;
        var rates = await _taxRateRepository.SearchAsync(skip, count);
        return View(new TaxRatesModel
        {
            Items = rates.ToList(),
            Page = page,
            Count = count
        });
    }

    public async Task<IActionResult> TaxRateAddEdit(string stripeTaxRateId = null)
    {
        if (string.IsNullOrWhiteSpace(stripeTaxRateId))
        {
            return View(new TaxRateAddEditModel());
        }

        var rate = await _taxRateRepository.GetByIdAsync(stripeTaxRateId);
        var model = new TaxRateAddEditModel()
        {
            StripeTaxRateId = stripeTaxRateId,
            Country = rate.Country,
            State = rate.State,
            PostalCode = rate.PostalCode,
            Rate = rate.Rate
        };

        return View(model);
    }

    [ValidateAntiForgeryToken]
    public async Task<IActionResult> TaxRateUpload(IFormFile file)
    {
        if (file == null || file.Length == 0)
        {
            throw new ArgumentNullException(nameof(file));
        }

        // Build rates and validate them first before updating DB & Stripe
        var taxRateUpdates = new List<TaxRate>();
        var currentTaxRates = await _taxRateRepository.GetAllActiveAsync();
        using var reader = new StreamReader(file.OpenReadStream());
        while (!reader.EndOfStream)
        {
            var line = await reader.ReadLineAsync();
            if (string.IsNullOrWhiteSpace(line))
            {
                continue;
            }
            var taxParts = line.Split(',');
            if (taxParts.Length < 2)
            {
                throw new Exception($"This line is not in the format of <postal code>,<rate>,<state code>,<country code>: {line}");
            }
            var postalCode = taxParts[0].Trim();
            if (string.IsNullOrWhiteSpace(postalCode))
            {
                throw new Exception($"'{line}' is not valid, the first element must contain a postal code.");
            }
            if (!decimal.TryParse(taxParts[1], out var rate) || rate <= 0M || rate > 100)
            {
                throw new Exception($"{taxParts[1]} is not a valid rate/decimal for {postalCode}");
            }
            var state = taxParts.Length > 2 ? taxParts[2] : null;
            var country = (taxParts.Length > 3 ? taxParts[3] : null);
            if (string.IsNullOrWhiteSpace(country))
            {
                country = "US";
            }
            var taxRate = currentTaxRates.FirstOrDefault(r => r.Country == country && r.PostalCode == postalCode) ??
                new TaxRate
                {
                    Country = country,
                    PostalCode = postalCode,
                    Active = true,
                };
            taxRate.Rate = rate;
            taxRate.State = state ?? taxRate.State;
            taxRateUpdates.Add(taxRate);
        }

        foreach (var taxRate in taxRateUpdates)
        {
            if (!string.IsNullOrWhiteSpace(taxRate.Id))
            {
                await _paymentService.UpdateTaxRateAsync(taxRate);
            }
            else
            {
                await _paymentService.CreateTaxRateAsync(taxRate);
            }
        }

        return RedirectToAction("TaxRate");
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> TaxRateAddEdit(TaxRateAddEditModel model)
    {
        var existingRateCheck = await _taxRateRepository.GetByLocationAsync(new TaxRate() { Country = model.Country, PostalCode = model.PostalCode });
        if (existingRateCheck.Any())
        {
            ModelState.AddModelError(nameof(model.PostalCode), "A tax rate already exists for this Country/Postal Code combination.");
        }

        if (!ModelState.IsValid)
        {
            return View(model);
        }

        var taxRate = new TaxRate()
        {
            Id = model.StripeTaxRateId,
            Country = model.Country,
            State = model.State,
            PostalCode = model.PostalCode,
            Rate = model.Rate
        };

        if (!string.IsNullOrWhiteSpace(model.StripeTaxRateId))
        {
            await _paymentService.UpdateTaxRateAsync(taxRate);
        }
        else
        {
            await _paymentService.CreateTaxRateAsync(taxRate);
        }

        return RedirectToAction("TaxRate");
    }

    public async Task<IActionResult> TaxRateArchive(string stripeTaxRateId)
    {
        if (!string.IsNullOrWhiteSpace(stripeTaxRateId))
        {
            await _paymentService.ArchiveTaxRateAsync(new TaxRate() { Id = stripeTaxRateId });
        }

        return RedirectToAction("TaxRate");
    }

    public async Task<IActionResult> StripeSubscriptions(StripeSubscriptionListOptions options)
    {
        options = options ?? new StripeSubscriptionListOptions();
        options.Limit = 10;
        options.Expand = new List<string>() { "data.customer", "data.latest_invoice" };
        options.SelectAll = false;

        var subscriptions = await _stripeAdapter.SubscriptionListAsync(options);

        options.StartingAfter = subscriptions.LastOrDefault()?.Id;
        options.EndingBefore = await StripeSubscriptionsGetHasPreviousPage(subscriptions, options) ?
            subscriptions.FirstOrDefault()?.Id :
            null;

        var model = new StripeSubscriptionsModel()
        {
            Items = subscriptions.Select(s => new StripeSubscriptionRowModel(s)).ToList(),
            Prices = (await _stripeAdapter.PriceListAsync(new Stripe.PriceListOptions() { Limit = 100 })).Data,
            TestClocks = await _stripeAdapter.TestClockListAsync(),
            Filter = options
        };
        return View(model);
    }

    [HttpPost]
    public async Task<IActionResult> StripeSubscriptions([FromForm] StripeSubscriptionsModel model)
    {
        if (!ModelState.IsValid)
        {
            model.Prices = (await _stripeAdapter.PriceListAsync(new Stripe.PriceListOptions() { Limit = 100 })).Data;
            model.TestClocks = await _stripeAdapter.TestClockListAsync();
            return View(model);
        }

        if (model.Action == StripeSubscriptionsAction.Export || model.Action == StripeSubscriptionsAction.BulkCancel)
        {
            var subscriptions = model.Filter.SelectAll ?
                await _stripeAdapter.SubscriptionListAsync(model.Filter) :
                model.Items.Where(x => x.Selected).Select(x => x.Subscription);

            if (model.Action == StripeSubscriptionsAction.Export)
            {
                return StripeSubscriptionsExport(subscriptions);
            }

            if (model.Action == StripeSubscriptionsAction.BulkCancel)
            {
                await StripeSubscriptionsCancel(subscriptions);
            }
        }
        else
        {
            if (model.Action == StripeSubscriptionsAction.PreviousPage || model.Action == StripeSubscriptionsAction.Search)
            {
                model.Filter.StartingAfter = null;
            }
            if (model.Action == StripeSubscriptionsAction.NextPage || model.Action == StripeSubscriptionsAction.Search)
            {
                model.Filter.EndingBefore = null;
            }
        }


        return RedirectToAction("StripeSubscriptions", model.Filter);
    }

    // This requires a redundant API call to Stripe because of the way they handle pagination.
    // The StartingBefore value has to be infered from the list we get, and isn't supplied by Stripe.
    private async Task<bool> StripeSubscriptionsGetHasPreviousPage(List<Stripe.Subscription> subscriptions, StripeSubscriptionListOptions options)
    {
        var hasPreviousPage = false;
        if (subscriptions.FirstOrDefault()?.Id != null)
        {
            var previousPageSearchOptions = new StripeSubscriptionListOptions()
            {
                EndingBefore = subscriptions.FirstOrDefault().Id,
                Limit = 1,
                Status = options.Status,
                CurrentPeriodEndDate = options.CurrentPeriodEndDate,
                CurrentPeriodEndRange = options.CurrentPeriodEndRange,
                Price = options.Price
            };
            hasPreviousPage = (await _stripeAdapter.SubscriptionListAsync(previousPageSearchOptions)).Count > 0;
        }
        return hasPreviousPage;
    }

    private async Task StripeSubscriptionsCancel(IEnumerable<Stripe.Subscription> subscriptions)
    {
        foreach (var s in subscriptions)
        {
            await _stripeAdapter.SubscriptionCancelAsync(s.Id);
            if (s.LatestInvoice?.Status == "open")
            {
                await _stripeAdapter.InvoiceVoidInvoiceAsync(s.LatestInvoiceId);
            }
        }
    }

    private FileResult StripeSubscriptionsExport(IEnumerable<Stripe.Subscription> subscriptions)
    {
        var fieldsToExport = subscriptions.Select(s => new
        {
            StripeId = s.Id,
            CustomerEmail = s.Customer?.Email,
            SubscriptionStatus = s.Status,
            InvoiceDueDate = s.CurrentPeriodEnd,
            SubscriptionProducts = s.Items?.Data.Select(p => p.Plan.Id)
        });

        var options = new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
            WriteIndented = true
        };

        var result = System.Text.Json.JsonSerializer.Serialize(fieldsToExport, options);
        var bytes = Encoding.UTF8.GetBytes(result);
        return File(bytes, "application/json", "StripeSubscriptionsSearch.json");
    }
}