/* eslint-disable @typescript-eslint/lines-between-class-members */
import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, Model } from 'sequelize';

import { BN } from '@ocap/util';

import { createIdGenerator } from '../../libs/util';
import logger from '../../libs/logger';

const nextTaxRateId = createIdGenerator('txr', 16);

// eslint-disable-next-line prettier/prettier
export class TaxRate extends Model<InferAttributes<TaxRate>, InferCreationAttributes<TaxRate>> {
  declare id: CreationOptional<string>;
  declare livemode: boolean;
  declare active: boolean;

  declare country: string; // ISO 3166-1 alpha-2 country code
  declare state?: string; // State, province, or region
  declare postal_code?: string; // Postal code or ZIP code (supports wildcards like "95*")
  declare tax_code?: string; // Product tax code (txcd_*)

  declare percentage: number; // Tax rate percentage (e.g., 8.5 for 8.5%)

  declare display_name: string; // Display name for the tax rate
  declare description?: string; // Description of the tax rate

  declare metadata?: Record<string, any>;

  declare created_at: CreationOptional<Date>;
  declare updated_at: CreationOptional<Date>;

  public static readonly GENESIS_ATTRIBUTES = {
    id: {
      type: DataTypes.STRING(30),
      primaryKey: true,
      allowNull: false,
      defaultValue: nextTaxRateId,
    },
    livemode: {
      type: DataTypes.BOOLEAN,
      allowNull: false,
    },
    active: {
      type: DataTypes.BOOLEAN,
      defaultValue: true,
    },
    country: {
      type: DataTypes.STRING(2),
      allowNull: false,
    },
    state: {
      type: DataTypes.STRING(50),
      allowNull: true,
    },
    postal_code: {
      type: DataTypes.STRING(20),
      allowNull: true,
    },
    tax_code: {
      type: DataTypes.STRING(20),
      allowNull: true,
    },
    percentage: {
      type: DataTypes.DECIMAL(5, 4), // Supports up to 99.9999%
      allowNull: false,
    },
    display_name: {
      type: DataTypes.STRING(100),
      allowNull: false,
    },
    description: {
      type: DataTypes.STRING(500),
      allowNull: true,
    },
    metadata: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    created_at: {
      type: DataTypes.DATE,
      defaultValue: DataTypes.NOW,
      allowNull: false,
    },
    updated_at: {
      type: DataTypes.DATE,
      defaultValue: DataTypes.NOW,
      allowNull: false,
    },
  };

  public static initialize(sequelize: any) {
    this.init(TaxRate.GENESIS_ATTRIBUTES, {
      sequelize,
      modelName: 'TaxRate',
      tableName: 'tax_rates',
      createdAt: 'created_at',
      updatedAt: 'updated_at',
      indexes: [
        {
          fields: ['country'],
        },
        {
          fields: ['country', 'state'],
        },
        {
          fields: ['country', 'state', 'postal_code'],
        },
      ],
    });
  }

  public static associate() {}

  /**
   * Find matching tax rate for given location and tax code
   *
   * Uses hierarchical scoring (lexicographic ordering) to match tax rates.
   * Priority: postal_code > state > tax_code
   *
   * Note: Country is pre-filtered to ensure postal codes are compared within
   * the correct national context (e.g., prevents US 90210 from matching CA 90210).
   *
   * Score ranges per dimension (0-100):
   *   - 100: Exact match
   *   - 50-95: Wildcard match (e.g., "902*" prefix matching)
   *   - 10: General fallback (rate has no restriction)
   *   - 5: Both empty
   */
  public static async findMatchingRate({
    country,
    state,
    postalCode,
    taxCode,
    livemode = true,
  }: {
    country: string;
    state?: string;
    postalCode?: string;
    taxCode?: string;
    livemode?: boolean;
  }): Promise<TaxRate | null> {
    const normalizedCountry = country?.trim().toUpperCase();
    const normalizedState = state?.trim().toLowerCase();
    const normalizedPostal = postalCode?.trim().toUpperCase();
    const normalizedTaxCode = taxCode?.trim().toLowerCase();

    if (!normalizedCountry) {
      logger.error('findMatchingRate: country is required');
      return null;
    }

    try {
      const { sequelize } = this;
      if (!sequelize) {
        throw new Error('Sequelize instance not found');
      }

      const candidates = await this.findAll({
        where: sequelize.where(sequelize.fn('UPPER', sequelize.col('country')), normalizedCountry) as any,
        attributes: {
          include: [[sequelize.literal('UPPER(country)'), 'normalized_country']],
        },
        order: [['updated_at', 'DESC']],
      });

      const activeCandidates = candidates.filter((rate) => rate.active && rate.livemode === livemode);

      if (!activeCandidates.length) {
        logger.debug('findMatchingRate: no active candidates found', {
          country: normalizedCountry,
          state: normalizedState,
          postal: normalizedPostal,
          taxCode: normalizedTaxCode,
          livemode,
          totalCandidates: candidates.length,
        });
        return null;
      }

      let bestMatch: TaxRate | null = null;
      let bestScore = { postal: 0, state: 0, tax: 0 };

      for (const rate of activeCandidates) {
        const rateTaxCode = rate.tax_code ? String(rate.tax_code).trim().toLowerCase() : null;
        const ratePostal = rate.postal_code ? String(rate.postal_code).trim().toUpperCase() : null;
        const rateState = rate.state ? String(rate.state).trim().toLowerCase() : null;

        let isValid = true;
        const scores = { postal: 0, state: 0, tax: 0 };

        // Postal Code Matching
        if (ratePostal && normalizedPostal) {
          if (ratePostal.endsWith('*')) {
            const prefix = ratePostal.slice(0, -1);
            if (normalizedPostal.startsWith(prefix)) {
              const prefixLength = Math.min(prefix.length, 20);
              scores.postal = 50 + prefixLength * 5;
            } else {
              isValid = false;
            }
          } else if (ratePostal === normalizedPostal) {
            scores.postal = 100;
          } else {
            isValid = false;
          }
        } else if (!ratePostal && !normalizedPostal) {
          scores.postal = 5;
        } else if (!ratePostal && normalizedPostal) {
          scores.postal = 10;
        } else if (ratePostal && !normalizedPostal) {
          isValid = false;
        }

        // State Matching
        if (isValid && rateState && normalizedState) {
          if (rateState === normalizedState) {
            scores.state = 100;
          } else {
            isValid = false;
          }
        } else if (isValid && !rateState && !normalizedState) {
          scores.state = 5;
        } else if (isValid && !rateState && normalizedState) {
          scores.state = 10;
        } else if (isValid && rateState && !normalizedState) {
          isValid = false;
        }

        // Tax Code Matching
        if (isValid) {
          if (normalizedTaxCode) {
            if (rateTaxCode === normalizedTaxCode) {
              scores.tax = 100;
            } else if (!rateTaxCode) {
              scores.tax = 10;
            } else {
              isValid = false;
            }
          } else if (!rateTaxCode) {
            scores.tax = 5;
          }
        }

        if (!isValid) {
          // eslint-disable-next-line no-continue
          continue;
        }

        // Hierarchical comparison (lexicographic ordering)
        // Use > not >= to prefer first match when scores are equal (newer rate due to DESC sort)
        const isBetter =
          scores.postal > bestScore.postal ||
          (scores.postal === bestScore.postal && scores.state > bestScore.state) ||
          (scores.postal === bestScore.postal && scores.state === bestScore.state && scores.tax > bestScore.tax);

        if (isBetter) {
          bestScore = scores;
          bestMatch = rate;
        }
      }

      if (!bestMatch) {
        logger.error('findMatchingRate: no matches found after scoring', {
          country: normalizedCountry,
          state: normalizedState,
          postal: normalizedPostal,
          taxCode: normalizedTaxCode,
          livemode,
          candidatesCount: activeCandidates.length,
        });
      } else {
        logger.info('findMatchingRate: found match', {
          taxRateId: bestMatch.id,
          score: bestScore,
          displayName: bestMatch.display_name,
          percentage: bestMatch.percentage,
        });
      }

      return bestMatch;
    } catch (error) {
      logger.error('findMatchingRate failed', {
        error,
        country: normalizedCountry,
        state: normalizedState,
        postal: normalizedPostal,
        taxCode: normalizedTaxCode,
        livemode,
      });
      return null;
    }
  }

  /**
   * Calculate tax amount from tax-inclusive total
   * Formula: tax = total × (rate / (100 + rate))
   */
  public static calculateTaxFromInclusive(total: string, percentage: number): string {
    const totalBN = new BN(total);
    if (totalBN.lte(new BN('0'))) return '0';

    const rateBN = new BN(percentage.toString()).mul(new BN('100'));
    const denominator = new BN('10000').add(rateBN);

    // tax = total * rate / (10000 + rate)
    const taxAmount = totalBN.mul(rateBN).div(denominator);
    return taxAmount.toString();
  }

  /**
   * Calculate subtotal from tax-inclusive total
   * Formula: subtotal = total - tax
   */
  public static calculateSubtotalFromInclusive(total: string, percentage: number): string {
    const totalBN = new BN(total);
    if (totalBN.lte(new BN('0'))) return total;

    const taxAmount = new BN(this.calculateTaxFromInclusive(total, percentage));
    return totalBN.sub(taxAmount).toString();
  }

  /**
   * Calculate tax amount for a single invoice item
   * Formula: tax = amount × (rate / 100)
   */
  public static calculateTaxForItem(amount: string, percentage: number): string {
    const amountBN = new BN(amount);
    if (amountBN.lte(new BN('0'))) return '0';

    const rateBN = new BN(percentage.toString()).mul(new BN('100'));

    // tax = amount * rate / 10000
    const taxAmount = amountBN.mul(rateBN).div(new BN('10000'));
    return taxAmount.toString();
  }

  /**
   * Calculate total tax for multiple invoice items
   */
  public static calculateTotalTax(
    items: Array<{ amount: string; tax_rate_id?: string; tax_rate?: { percentage: number } }>
  ): string {
    return items.reduce((total, item) => {
      if (!item.tax_rate_id || !item.tax_rate) return total;

      const itemTax = this.calculateTaxForItem(item.amount, item.tax_rate.percentage);
      return new BN(total).add(new BN(itemTax)).toString();
    }, '0');
  }
}

export type TTaxRate = InferAttributes<TaxRate>;
