import { BindingScope, inject, injectable } from '@loopback/core';
import { juggler } from '@loopback/repository';
import { HttpErrors } from '@loopback/rest';

import { LbxInvoiceBindings } from '../keys';
import { NumberInvoices } from '../models';
import { AddressData } from '../models/invoice/address-data.model';
import { BaseInvoiceRepository, NumberInvoicesRepository } from '../repositories';

/**
 * Handles generating unique and consecutive numbers for invoices.
 */
@injectable({ scope: BindingScope.TRANSIENT })
export class BaseInvoiceNumberService {
    /**
     * The number of digits used to generate the consecutive number.
     * @default 4
     */
    protected readonly NUMBER_OF_DIGITS: number = 4;

    /**
     * The separator for the parts of the invoice number.
     * @default '-'
     */
    protected readonly SEPARATOR: string = '-';

    /**
     * How many characters of the company name should be used in the invoice number.
     * @default 6
     */
    protected readonly NUMBER_COMPANY_ABBREVIATION_CHARACTERS: number = 6;

    /**
     * How many characters of the private customer first and last name should be used in the invoice number.
     * @default 3
     */
    protected readonly NUMBER_PRIVATE_CUSTOMER_ABBREVIATION_CHARACTERS: number = 3;

    constructor(
        @inject(LbxInvoiceBindings.INVOICE_REPOSITORY)
        private readonly invoiceRepository: BaseInvoiceRepository,
        @inject(LbxInvoiceBindings.NUMBER_INVOICES_REPOSITORY)
        private readonly numberInvoicesRepository: NumberInvoicesRepository
    ) {}

    /**
     * Generates a new invoice number.
     * @param recipientId - The id of the recipient of the invoice.
     * @param invoiceAddress - The address data of the customer.
     * @param transaction - An optional transaction from outside to make sure any changes only apply when the transaction is committed.
     * @param nameAbbreviation - An optional name abbreviation if you don't want to generate one.
     * @returns A promise of the new invoice number.
     */
    async generateInvoiceNumber(
        recipientId: string,
        invoiceAddress: AddressData,
        transaction?: juggler.Transaction,
        nameAbbreviation?: string
    ): Promise<string> {
        const currentYear: string = `${new Date().getFullYear()}`;
        const customerNameAbbreviation: string = nameAbbreviation ?? this.getCustomerNameAbbreviation(invoiceAddress);
        const consecutiveNumber: string = await this.getConsecutiveNumber(recipientId, transaction);
        const result: string = `${currentYear}${this.SEPARATOR}${customerNameAbbreviation}${this.SEPARATOR}${consecutiveNumber}`;
        await this.validateInvoiceNumber(result);
        return result;
    }

    /**
     * Generates a temporary invoice number with the prefix temp.
     * This does not increase the number of invoices which can be helpful if you create an invoice that might not be sent out.
     * @param recipientId - The id of the recipient of the invoice.
     * @param invoiceAddress - The address data of the customer.
     * @param nameAbbreviation - An optional name abbreviation if you don't want to generate one.
     * @returns A promise of the new temporary invoice number.
     */
    async generateTemporaryInvoiceNumber(recipientId: string, invoiceAddress: AddressData, nameAbbreviation?: string): Promise<string> {
        const currentYear: string = `${new Date().getFullYear()}`;
        const customerNameAbbreviation: string = nameAbbreviation ?? this.getCustomerNameAbbreviation(invoiceAddress);
        const consecutiveNumber: string = await this.getTemporaryConsecutiveNumber(recipientId);

        // eslint-disable-next-line stylistic/max-len
        const result: string = `TEMP${this.SEPARATOR}${currentYear}${this.SEPARATOR}${customerNameAbbreviation}${this.SEPARATOR}${consecutiveNumber}`;
        if (!await this.invoiceRepository.findOne({ where: { number: result } })) {
            return result;
        }

        let suffix: number = 2;
        while (await this.invoiceRepository.findOne({ where: { number: `${result}${this.SEPARATOR}${suffix}` } })) {
            suffix++;
        }
        return `${result}${this.SEPARATOR}${suffix}`;
    }

    /**
     * Gets the temporary consecutive number of the invoices in this year and for the provided recipientId.
     * Prefixes it with zeroes until the result has the same length as this.NUMBER_OF_DIGITS.
     * @param recipientId - The id of the recipient of the invoice.
     * @returns The number of invoices over a year filled up with zeroes to match this.NUMBER_OF_DIGITS.
     */
    protected async getTemporaryConsecutiveNumber(recipientId: string): Promise<string> {
        const currentYear: number = new Date(Date.now()).getFullYear();
        let numberOfInvoices: NumberInvoices | null = await this.numberInvoicesRepository.findOne({
            where: {
                year: currentYear,
                recipientId: recipientId
            }
        });
        if (numberOfInvoices) {
            numberOfInvoices.number++;
        }
        else {
            numberOfInvoices = {
                number: 1,
                year: currentYear,
                recipientId: recipientId
            } as NumberInvoices;
        }

        const consecutiveNumber: string = `${numberOfInvoices.number}`;
        if (consecutiveNumber.length > this.NUMBER_OF_DIGITS) {
            return consecutiveNumber;
        }
        const difference: number = this.NUMBER_OF_DIGITS - consecutiveNumber.length;
        let prefix: string = '';
        for (let i: number = 0; i < difference; i++) {
            prefix = `${prefix}0`;
        }
        return `${prefix}${consecutiveNumber}`;
    }

    /**
     * Gets the consecutive number of the invoices in this year.
     * Prefixes it with zeroes until the result has the same length as this.NUMBER_OF_DIGITS.
     * @param recipientId - The id of the recipient of the invoice.
     * @param transaction - An optional transaction from outside to make sure any changes only apply when the transaction is committed.
     * @returns The number of invoices over a year filled up with zeroes to match this.NUMBER_OF_DIGITS.
     */
    protected async getConsecutiveNumber(recipientId: string, transaction?: juggler.Transaction): Promise<string> {
        const currentYear: number = new Date(Date.now()).getFullYear();
        let numberOfInvoices: NumberInvoices | null = await this.numberInvoicesRepository.findOne({
            where: {
                year: currentYear,
                recipientId: recipientId
            }
        });
        if (numberOfInvoices) {
            numberOfInvoices.number++;
            await this.numberInvoicesRepository.updateById(numberOfInvoices.id, numberOfInvoices, { transaction: transaction });
        }
        else {
            numberOfInvoices = await this.numberInvoicesRepository.create(
                {
                    number: 1,
                    year: currentYear,
                    recipientId: recipientId
                },
                { transaction: transaction }
            );
        }

        const consecutiveNumber: string = `${numberOfInvoices.number}`;
        if (consecutiveNumber.length > this.NUMBER_OF_DIGITS) {
            return consecutiveNumber;
        }
        const difference: number = this.NUMBER_OF_DIGITS - consecutiveNumber.length;
        let prefix: string = '';
        for (let i: number = 0; i < difference; i++) {
            prefix = `${prefix}0`;
        }
        return `${prefix}${consecutiveNumber}`;
    }

    /**
     * Gets the customer name abbreviation.
     * @param invoiceAddress - The address data to get the name abbreviation from.
     * @returns The first char of first and last name for private customers
     * and the first two chars of the company name for company customers.
     */
    protected getCustomerNameAbbreviation(invoiceAddress: AddressData): string {
        if (invoiceAddress.company && invoiceAddress.companyName) {
            return this.getCompanyNameAbbreviation(invoiceAddress.companyName);
        }
        return this.getPrivateCustomerAbbreviation(invoiceAddress);
    }

    private getPrivateCustomerAbbreviation(invoiceAddress: AddressData): string {
        let res: string = '';
        for (let i: number = 0; i < this.NUMBER_PRIVATE_CUSTOMER_ABBREVIATION_CHARACTERS; i++) {
            if (invoiceAddress.firstName[i]) {
                res += invoiceAddress.firstName[i];
            }
        }
        for (let i: number = 0; i < this.NUMBER_PRIVATE_CUSTOMER_ABBREVIATION_CHARACTERS; i++) {
            if (invoiceAddress.lastName[i]) {
                res += invoiceAddress.lastName[i];
            }
        }
        return res.toUpperCase();
    }

    private getCompanyNameAbbreviation(companyName: string): string {
        companyName = companyName.replaceAll(/\s/g, '');
        companyName = companyName.replaceAll(/[^\da-z]/gi, '');
        let res: string = '';
        for (let i: number = 0; i < this.NUMBER_COMPANY_ABBREVIATION_CHARACTERS; i++) {
            if (companyName[i]) {
                res += companyName[i];
            }
        }
        return res.toUpperCase();
    }

    // TODO: Is this still needed?
    /**
     * Validates the given invoice number.
     * @param invoiceNumber - The invoice number to validate.
     */
    protected async validateInvoiceNumber(invoiceNumber: string): Promise<void> {
        if (await this.invoiceRepository.findOne({ where: { number: invoiceNumber } })) {
            throw new HttpErrors.Conflict(`The generated invoice-number ${invoiceNumber} already exists!`);
        }
    }
}