/* eslint-disable @typescript-eslint/lines-between-class-members */
import { BN } from '@ocap/util';
import cloneDeep from 'lodash/cloneDeep';
import padStart from 'lodash/padStart';
import {
  CreationOptional,
  DataTypes,
  FindOptions,
  InferAttributes,
  InferCreationAttributes,
  Model,
  Op,
} from 'sequelize';

import merge from 'lodash/merge';
import { getLock } from '../../libs/lock';
import { createEvent } from '../../libs/audit';
import CustomError from '../../libs/error';
import { createCodeGenerator, createIdGenerator } from '../../libs/util';
import type { CustomerAddress, CustomerPreferences, CustomerShipping } from './types';

export const nextCustomerId = createIdGenerator('cus', 14);
export const nextInvoicePrefix = createCodeGenerator('', 8);

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

  declare did: string;
  declare description: string;
  declare name: string;
  declare email: string;
  declare phone: string;

  declare address?: CustomerAddress;
  declare shipping?: CustomerShipping;

  declare balance?: string; // synced with stripe
  declare token_balance?: Record<string, string>; // token balances
  // declare token_limits?: Record<string, string>; // token limits
  declare currency_id?: string;
  declare delinquent: boolean;
  declare discount_id?: string;

  declare metadata: Record<string, any>;

  declare invoice_credit_balance?: any;
  declare invoice_prefix?: string;
  declare invoice_settings?: {
    custom_fields?: {
      name: string;
      value: string;
    }[];
    default_payment_method?: string;
    footer?: string;
  };
  declare next_invoice_sequence?: number;

  // Per-channel UUID for IAP appAccountToken / obfuscatedAccountId mapping (D-004)
  declare app_store_uuid?: string;
  declare google_play_uuid?: string;

  // TODO: following fields not supported
  // declare preferred_locales?: string[];
  // declare tax_exempt?: LiteralUnion<'exempt' | 'none' | 'reverse', string>;
  // declare tax_ids?: any;

  declare created_at: CreationOptional<Date>;
  declare updated_at: CreationOptional<Date>;
  declare last_sync_at?: number;
  declare preference?: CustomerPreferences;

  public static readonly GENESIS_ATTRIBUTES = {
    id: {
      type: DataTypes.STRING(18),
      primaryKey: true,
      allowNull: false,
      defaultValue: nextCustomerId,
    },
    livemode: {
      type: DataTypes.BOOLEAN,
      allowNull: false,
    },
    did: {
      type: DataTypes.STRING(40),
      allowNull: false,
      unique: true,
    },
    address: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    description: {
      type: DataTypes.STRING(512),
      allowNull: true,
    },
    name: {
      type: DataTypes.STRING(64),
      allowNull: true,
    },
    email: {
      type: DataTypes.STRING(128),
      allowNull: true,
    },
    phone: {
      type: DataTypes.STRING(32),
      allowNull: true,
    },
    shipping: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    balance: {
      type: DataTypes.STRING(32),
      defaultValue: '0',
    },
    currency_id: {
      type: DataTypes.STRING(15),
      allowNull: true,
    },
    delinquent: {
      type: DataTypes.BOOLEAN,
      allowNull: false,
    },
    discount_id: {
      type: DataTypes.STRING(32),
      allowNull: true,
    },
    metadata: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    invoice_credit_balance: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    invoice_prefix: {
      type: DataTypes.STRING(30),
      allowNull: true,
    },
    invoice_settings: {
      type: DataTypes.JSON,
      allowNull: true,
    },
    next_invoice_sequence: {
      type: DataTypes.NUMBER,
      defaultValue: 1,
    },
    app_store_uuid: {
      type: DataTypes.STRING(36),
      allowNull: true,
      unique: true,
    },
    google_play_uuid: {
      type: DataTypes.STRING(36),
      allowNull: true,
      unique: true,
    },
    created_at: {
      type: DataTypes.DATE,
      defaultValue: DataTypes.NOW,
      allowNull: false,
    },
    updated_at: {
      type: DataTypes.DATE,
      defaultValue: DataTypes.NOW,
      allowNull: false,
    },
  };

  public async getInvoiceNumber() {
    // Optimized path for CF Workers: atomic UPDATE...RETURNING in 1 D1 round trip.
    // Original lock-based approach: lock.acquire → reload → increment(UPDATE + reload) → release
    //   = 2 D1 round trips for lock + 3 D1 round trips for data = ~1000ms at ~350ms/RT
    // Optimized: single UPDATE...RETURNING = 1 D1 round trip = ~350ms
    //
    // SQLite serializes writes at the DB level, so the application-level lock is
    // redundant for both D1 and Blocklet Server's SQLite.
    const d1 = (globalThis as any).__CF_ENV__?.DB;
    if (d1) {
      try {
        const sql =
          // eslint-disable-next-line @typescript-eslint/quotes
          'UPDATE "customers" SET "next_invoice_sequence" = COALESCE("next_invoice_sequence", 1) + 1 WHERE "id" = ? RETURNING "next_invoice_sequence" - 1 as "prev_sequence"';
        const result = await d1.prepare(sql).bind(this.id).first();
        const sequence = (result as any)?.prev_sequence ?? 1;
        return `${this.invoice_prefix}-${padStart(sequence.toString(), 4, '0')}`;
      } catch (err) {
        console.warn('[getInvoiceNumber] D1 RETURNING failed, falling back:', (err as any)?.message);
      }
    }

    // Blocklet Server path (or D1 fallback): use lock-based approach
    const lock = getLock(`${this.id}-invoice-number`);
    await lock.acquire();
    await this.reload();
    const sequence = this.next_invoice_sequence || 1;
    await this.increment('next_invoice_sequence', { by: 1 });
    lock.release();

    return `${this.invoice_prefix}-${padStart(sequence.toString(), 4, '0')}`;
  }

  public async getSummary(livemode?: boolean) {
    const { PaymentIntent, Refund, Invoice } = this.sequelize.models;
    const [paid, [due], refunded] = await Promise.all([
      // @ts-ignore
      PaymentIntent!.getPaidAmountByCustomer(this.id),
      // @ts-ignore
      Invoice!.getUncollectibleAmount({ customerId: this.id, livemode }),
      // @ts-ignore
      Refund!.getRefundAmountByCustomer(this.id),
    ]);

    return { paid, due, refunded };
  }

  public async canMakeNewPurchase(excludedInvoiceId: string = '') {
    const { Invoice } = this.sequelize.models;
    // @ts-ignore
    const [summary] = await Invoice!.getUncollectibleAmount({ customerId: this.id, excludedInvoiceId });
    return Object.entries(summary).every(([, amount]) => new BN(amount as string).lte(new BN(0)));
  }

  public getBalanceToApply(currencyId: string, amount: string) {
    const tokens = this.token_balance || {};
    const balance = tokens[currencyId] || '0';
    return new BN(balance).lt(new BN(amount)) ? balance : amount;
  }

  public async decreaseTokenBalance(currencyId: string, amount: string, dryRun: boolean = false) {
    if (amount === '0') {
      return { starting: {}, ending: {} };
    }

    const tokens = this.token_balance || {};
    const balance = tokens[currencyId] || '0';
    if (new BN(balance).lt(new BN(amount))) {
      throw new CustomError('INSUFFICIENT_BALANCE', 'Insufficient customer balance');
    }

    const starting = cloneDeep(tokens);
    // NOTE: new object is required to trick sequelize to update the field
    const ending = { ...starting, [currencyId]: new BN(balance).sub(new BN(amount)).toString() };
    if (!dryRun) {
      await this.update({ token_balance: ending });
    }
    return { starting, ending };
  }

  public async increaseTokenBalance(currencyId: string, amount: string, dryRun: boolean = false) {
    const tokens = this.token_balance || {};
    const balance = tokens[currencyId] || '0';
    const starting = cloneDeep(tokens);
    // NOTE: new object is required to trick sequelize to update the field
    const ending = { ...starting, [currencyId]: new BN(balance).add(new BN(amount)).toString() };
    if (!dryRun) {
      await this.update({ token_balance: ending });
    }
    return { starting, ending };
  }

  public static initialize(sequelize: any) {
    this.init(
      {
        ...Customer.GENESIS_ATTRIBUTES,
        token_balance: {
          type: DataTypes.JSON,
          defaultValue: {},
        },
        last_sync_at: {
          type: DataTypes.INTEGER,
          allowNull: true,
        },
        preference: {
          type: DataTypes.JSON,
          allowNull: true,
          defaultValue: {
            notification: {
              frequency: 'monthly',
              schedule: {
                time: '10:00',
                date: 1,
              },
            },
          },
        },
      },
      {
        sequelize,
        modelName: 'Customer',
        tableName: 'customers',
        createdAt: 'created_at',
        updatedAt: 'updated_at',
        hooks: {
          afterCreate: (model: Customer, options) => {
            createEvent('Customer', 'customer.created', model, options).catch(console.error);
          },
          afterUpdate: (model: Customer, options) => {
            createEvent('Customer', 'customer.updated', model, options).catch(console.error);
          },
          afterDestroy: (model: Customer, options) => {
            createEvent('Customer', 'customer.deleted', model, options).catch(console.error);
          },
        },
      }
    );
  }

  public static associate(models: any) {
    this.hasOne(models.PaymentCurrency, {
      sourceKey: 'currency_id',
      foreignKey: 'id',
      as: 'currency',
    });
    this.hasMany(models.PaymentIntent, {
      foreignKey: 'customer_id',
      as: 'payments',
    });
    this.hasMany(models.Subscription, {
      foreignKey: 'customer_id',
      as: 'subscriptions',
    });
    this.hasMany(models.Invoice, {
      foreignKey: 'customer_id',
      as: 'invoices',
    });
    this.hasMany(models.CreditGrant, {
      foreignKey: 'customer_id',
      as: 'credit_grants',
    });
    this.hasMany(models.CreditTransaction, {
      foreignKey: 'customer_id',
      as: 'credit_transactions',
    });
  }

  public static findByPkOrDid(id: string, options: FindOptions<Customer> = {}) {
    return this.findOne({
      where: { [Op.or]: [{ id }, { did: id }] },
      ...options,
    });
  }

  public static getInvoicePrefix() {
    return nextInvoicePrefix();
  }

  public static formatAddressFromUser(user: any, customer?: Customer) {
    return {
      country: user?.address?.country || customer?.address?.country || '',
      state: user?.address?.province || customer?.address?.state || '',
      city: user?.address?.city || customer?.address?.city || '',
      line1: user?.address?.line1 || customer?.address?.line1 || '',
      line2: user?.address?.line2 || customer?.address?.line2 || '',
      postal_code: user?.address?.postalCode || customer?.address?.postal_code || '',
    };
  }

  public static formatAddressFromCustomer(customer: Customer) {
    return {
      country: customer.address?.country || '',
      province: customer.address?.state || '',
      city: customer.address?.city || '',
      line1: customer.address?.line1 || '',
      line2: customer.address?.line2 || '',
      postalCode: customer.address?.postal_code || '',
    };
  }

  public static formatUpdateAddress(address: CustomerAddress, customer: Customer): CustomerAddress {
    const defaultAddress = {
      country: 'us',
      state: '',
      city: '',
      line1: '',
      line2: '',
      postal_code: '',
    };

    return merge(
      defaultAddress,
      customer.address || {},
      address
        ? {
            country: address.country?.toLowerCase(),
            state: address.state,
            city: address.city,
            line1: address.line1,
            line2: address.line2,
            postal_code: address.postal_code,
          }
        : {}
    );
  }
}

export type TCustomer = InferAttributes<Customer>;
