import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { and, desc, eq, inArray, lt, sql } from 'drizzle-orm';
import { DbService } from '@/common/db/db.service';
import { tenants, subscriptions, warehouses, inventoryBalances, locations, aiProposals } from '@/db/schema';
import { EventBus, DomainEvent } from '@/common/events/event-bus.service';
import { runWithContext, systemContext } from '@/common/context/request-context';
import { loadEnv } from '@/config/env';
import { todayIso } from '@/common/util';
import { StorageBillingService } from '@/modules/billing/storage-billing.service';
import { ReplenishmentService } from '@/modules/outbound/replenishment.service';
import { LedgerService } from '@/modules/inventory/ledger.service';
import { PlatformService } from '@/modules/platform/platform.service';

/**
 * Scheduled maintenance, per tenant:
 *  - hourly:   end-of-day storage snapshot for every warehouse once its local day-end hour has passed
 *  - 15 min:   min/max replenishment
 *  - daily:    expiry sweep (AVAILABLE -> EXPIRED), zero-balance purge, AI proposal expiry
 *  - on event: usage metering (orders shipped, ASNs received, tasks completed)
 */
@Injectable()
export class ScheduledJobs implements OnModuleInit {
  private readonly logger = new Logger(ScheduledJobs.name);
  private readonly enabled = loadEnv().JOBS_ENABLED;

  constructor(
    private readonly db: DbService,
    private readonly bus: EventBus,
    private readonly storage: StorageBillingService,
    private readonly replenishment: ReplenishmentService,
    private readonly ledger: LedgerService,
    private readonly platform: PlatformService,
  ) {}

  onModuleInit() {
    this.bus.subscribe('order.shipped', (e) => this.meter(e, 'ORDERS_SHIPPED'));
    this.bus.subscribe('order.short_closed', (e) => this.meter(e, 'ORDERS_SHIPPED'));
    this.bus.subscribe('asn.closed', (e) => this.meter(e, 'ASN_RECEIVED'));
    this.bus.subscribe('task.completed', (e) => this.meter(e, 'TASKS_COMPLETED'));
  }

  private async meter(e: DomainEvent, metric: 'ORDERS_SHIPPED' | 'ASN_RECEIVED' | 'TASKS_COMPLETED') {
    if ((e.attempt ?? 0) > 0) return; // retries of a partially failed dispatch must not count twice
    await this.db.runBypass((tx) => this.platform.recordUsage(e.tenantId, metric, 1, e.occurredAt.slice(0, 10), tx));
  }

  private async activeTenants() {
    return this.db.runBypass((tx) => tx.select({ id: tenants.id, timezone: tenants.timezone }).from(tenants).where(inArray(tenants.status, ['TRIAL', 'ACTIVE'])));
  }

  @Cron('7 * * * *')
  async hourlySnapshots() {
    if (!this.enabled) return;
    for (const t of await this.activeTenants()) {
      await runWithContext(systemContext(t.id), async () => {
        const whs = await this.db.run((tx) => tx.select().from(warehouses).where(eq(warehouses.status, 'ACTIVE')));
        for (const w of whs) {
          const dayEnd = Number((w.settings as Record<string, unknown>).dayEndHour ?? 23);
          const localHour = Number(new Intl.DateTimeFormat('en-GB', { timeZone: w.timezone, hour: '2-digit', hour12: false }).format(new Date()));
          const localDate = todayIso(w.timezone);
          if (localHour < dayEnd) continue;
          try {
            await this.db.run((tx) => this.storage.snapshotWarehouse(tx, w.id, localDate));
          } catch (e) {
            this.logger.error(`Snapshot failed for warehouse ${w.code}: ${(e as Error).message}`);
          }
        }
      });
    }
  }

  @Cron('*/15 * * * *')
  async replenish() {
    if (!this.enabled) return;
    for (const t of await this.activeTenants()) {
      await runWithContext(systemContext(t.id), async () => {
        const whs = await this.db.run((tx) => tx.select({ id: warehouses.id, code: warehouses.code }).from(warehouses).where(eq(warehouses.status, 'ACTIVE')));
        for (const w of whs) {
          try {
            const r = await this.db.run((tx) => this.replenishment.runInTx(tx, w.id));
            if (r.created) this.logger.log(`Replenishment ${w.code}: ${r.created} tasks`);
          } catch (e) {
            this.logger.error(`Replenishment failed for ${w.code}: ${(e as Error).message}`);
          }
        }
      });
    }
  }

  @Cron('30 1 * * *')
  async daily() {
    if (!this.enabled) return;
    for (const t of await this.activeTenants()) {
      await runWithContext(systemContext(t.id), async () => {
        const step = async (name: string, fn: () => Promise<unknown>) => {
          try {
            await fn();
          } catch (e) {
            this.logger.error(`Daily ${name} failed for tenant ${t.id}: ${(e as Error).message}`);
          }
        };
        await step('expiry sweep', () => this.expirySweep(t.timezone));
        await step('zero-balance purge', async () => {
          const purged = await this.db.run((tx) => this.ledger.purgeZeroBalances(tx));
          if (purged) this.logger.log(`Purged ${purged} zero balances`);
        });
        await step('AI proposal expiry', () => this.db.run((tx) => tx.update(aiProposals).set({ status: 'EXPIRED' }).where(and(eq(aiProposals.status, 'PROPOSED'), lt(aiProposals.expiresAt, new Date())))));
        await step('trial expiry', () => this.expireTrial(t.id));
      });
    }
  }

  /** TRIAL tenants past their trial end are suspended (users get 403 TENANT_SUSPENDED until a plan is activated). */
  private async expireTrial(tenantId: string) {
    await this.db.runBypass(async (tx) => {
      const [t] = await tx.select({ status: tenants.status }).from(tenants).where(eq(tenants.id, tenantId)).limit(1);
      if (!t || t.status !== 'TRIAL') return;
      const [sub] = await tx.select({ trialEndsAt: subscriptions.trialEndsAt, status: subscriptions.status }).from(subscriptions).where(eq(subscriptions.tenantId, tenantId)).orderBy(desc(subscriptions.createdAt)).limit(1);
      if (sub?.trialEndsAt && sub.trialEndsAt < new Date() && sub.status !== 'ACTIVE') {
        await tx.update(tenants).set({ status: 'SUSPENDED' }).where(eq(tenants.id, tenantId));
        this.logger.warn(`Tenant ${tenantId} suspended: trial ended ${sub.trialEndsAt.toISOString().slice(0, 10)}`);
      }
    });
  }

  /** Stock past its expiry date can no longer be allocated: move it to EXPIRED status. */
  async expirySweep(timezone: string) {
    const today = todayIso(timezone);
    await this.db.run(async (tx) => {
      const rows = await tx
        .select({ b: inventoryBalances })
        .from(inventoryBalances)
        .innerJoin(locations, eq(locations.id, inventoryBalances.locationId))
        .where(and(eq(inventoryBalances.status, 'AVAILABLE'), sql`${inventoryBalances.expiryDate} < ${today}`, sql`${inventoryBalances.qtyOnHand} > ${inventoryBalances.qtyAllocated}`));
      for (const { b } of rows) {
        const free = Number(b.qtyOnHand) - Number(b.qtyAllocated);
        if (free <= 0) continue;
        await this.ledger.post(tx, {
          type: 'STATUS_CHANGE', qty: free, warehouseId: b.warehouseId, storerId: b.storerId, skuId: b.skuId,
          from: { locationId: b.locationId, lpnId: b.lpnId, status: 'AVAILABLE' }, to: { locationId: b.locationId, lpnId: b.lpnId, status: 'EXPIRED' },
          lotNumber: b.lotNumber, expiryDate: b.expiryDate, refType: 'EXPIRY_SWEEP', notes: `Expired on ${b.expiryDate}`, firstReceivedAt: b.firstReceivedAt,
        });
      }
      if (rows.length) this.logger.log(`Expiry sweep: ${rows.length} balances moved to EXPIRED`);
    });
  }

  /** Backfill snapshots for a date range (support tool after an outage). */
  async backfillSnapshots(tenantId: string, warehouseId: string, from: string, to: string) {
    await runWithContext(systemContext(tenantId), () =>
      this.db.run(async (tx) => {
        for (let d = from; d <= to; d = new Date(new Date(`${d}T00:00:00Z`).getTime() + 86400000).toISOString().slice(0, 10)) {
          await this.storage.snapshotWarehouse(tx, warehouseId, d);
        }
      }),
    );
  }

}
