import { Injectable, Logger } from '@nestjs/common';
import * as argon2 from 'argon2';
import { eq } from 'drizzle-orm';
import { ProvisionTenantDto, SYSTEM_ROLES, REASON_CODE_TYPE } from '@wms/shared';
import { DbService, Tx } from '@/common/db/db.service';
import { plans, tenants, subscriptions, featureFlags, users, roles, userRoles, warehouses, userWarehouses, zones, locations, reasonCodes } from '@/db/schema';
import { AuditService } from '@/common/audit/audit.service';
import { DomainError, NotFoundError } from '@/common/errors/domain-error';

/** Reason codes every tenant starts with. Tenants can add their own. */
const DEFAULT_REASON_CODES: { type: (typeof REASON_CODE_TYPE)[number]; code: string; description: string; requiresApproval?: boolean; requiresPhoto?: boolean }[] = [
  { type: 'DAMAGE', code: 'DMG-TRANSIT', description: 'Damaged in transit', requiresPhoto: true },
  { type: 'DAMAGE', code: 'DMG-HANDLING', description: 'Damaged during handling', requiresPhoto: true },
  { type: 'DAMAGE', code: 'WET', description: 'Water damage / wet cartons', requiresPhoto: true },
  { type: 'DAMAGE', code: 'TEMP', description: 'Temperature excursion', requiresPhoto: true },
  { type: 'REJECTION', code: 'EXPIRED', description: 'Expired or below minimum shelf life' },
  { type: 'REJECTION', code: 'NOT-ON-PL', description: 'Item not on packing list' },
  { type: 'REJECTION', code: 'LABEL', description: 'Labelling non-compliant' },
  { type: 'ADJUSTMENT', code: 'COUNT', description: 'Cycle count variance', requiresApproval: true },
  { type: 'ADJUSTMENT', code: 'FOUND', description: 'Stock found', requiresApproval: true },
  { type: 'ADJUSTMENT', code: 'LOST', description: 'Stock lost / unaccounted', requiresApproval: true },
  { type: 'ADJUSTMENT', code: 'WRITE-OFF', description: 'Write-off (expired / destroyed)', requiresApproval: true },
  { type: 'ADJUSTMENT', code: 'DATA-FIX', description: 'Data correction', requiresApproval: true },
  { type: 'HOLD', code: 'QC', description: 'Quality control hold' },
  { type: 'HOLD', code: 'CUSTOMER', description: 'Customer requested hold' },
  { type: 'HOLD', code: 'RECALL', description: 'Product recall' },
  { type: 'HOLD', code: 'DISPUTE', description: 'Under dispute / investigation' },
  { type: 'RETURN', code: 'CUST-REJECT', description: 'Rejected by consignee' },
  { type: 'RETURN', code: 'WRONG-ITEM', description: 'Wrong item delivered' },
  { type: 'RETURN', code: 'UNDELIVERED', description: 'Undelivered / address issue' },
  { type: 'CANCELLATION', code: 'CUST-CANCEL', description: 'Cancelled by customer' },
  { type: 'CANCELLATION', code: 'NO-STOCK', description: 'No stock available' },
  { type: 'SHORT_PICK', code: 'EMPTY-LOC', description: 'Location empty' },
  { type: 'SHORT_PICK', code: 'DAMAGED', description: 'Stock damaged at pick' },
  { type: 'COUNT_VARIANCE', code: 'MISPLACED', description: 'Stock misplaced' },
  { type: 'VAS', code: 'LABELLING', description: 'Labelling / stickering' },
  { type: 'VAS', code: 'PALLETISATION', description: 'Palletisation and wrapping' },
];

/**
 * Creates a tenant with everything needed to start working:
 * subscription, feature flags from the plan, system roles, admin user,
 * first warehouse with standard zones and system locations, reason codes.
 */
@Injectable()
export class TenantProvisioningService {
  private readonly logger = new Logger(TenantProvisioningService.name);

  constructor(
    private readonly db: DbService,
    private readonly audit: AuditService,
  ) {}

  async provision(dto: ProvisionTenantDto) {
    return this.db.runBypass(async (tx) => {
      const [plan] = await tx.select().from(plans).where(eq(plans.code, dto.planCode)).limit(1);
      if (!plan) throw new NotFoundError('Plan', dto.planCode);
      const [existing] = await tx.select({ id: tenants.id }).from(tenants).where(eq(tenants.code, dto.code)).limit(1);
      if (existing) throw new DomainError('DUPLICATE', `Tenant code ${dto.code} already exists`);

      const [tenant] = await tx
        .insert(tenants)
        .values({ code: dto.code, name: dto.name, status: dto.trialDays > 0 ? 'TRIAL' : 'ACTIVE', timezone: dto.timezone, currency: dto.currency, locale: dto.locale })
        .returning();

      const now = new Date();
      const trialEnds = dto.trialDays > 0 ? new Date(now.getTime() + dto.trialDays * 86400000) : null;
      await tx.insert(subscriptions).values({ tenantId: tenant.id, planId: plan.id, status: trialEnds ? 'TRIAL' : 'ACTIVE', startsAt: now, trialEndsAt: trialEnds, seats: Number(plan.limits['maxUsers'] ?? 10) });

      if (plan.features.length) {
        await tx.insert(featureFlags).values(plan.features.map((key) => ({ tenantId: tenant.id, key, enabled: true })));
      }

      const roleIds = await this.createSystemRoles(tenant.id, tx);
      const warehouse = await this.createWarehouse(tenant.id, dto.firstWarehouse.code, dto.firstWarehouse.name, dto.firstWarehouse.timezone ?? dto.timezone, tx);

      const [admin] = await tx
        .insert(users)
        .values({ tenantId: tenant.id, email: dto.adminEmail.toLowerCase(), passwordHash: await argon2.hash(dto.adminPassword), name: dto.adminName, isTenantAdmin: true, status: 'ACTIVE', locale: dto.locale })
        .returning();
      await tx.insert(userRoles).values({ userId: admin.id, roleId: roleIds['ADMIN'], warehouseId: null });
      await tx.insert(userWarehouses).values({ userId: admin.id, warehouseId: warehouse.id, isDefault: true });

      await tx.insert(reasonCodes).values(DEFAULT_REASON_CODES.map((r) => ({ tenantId: tenant.id, ...r })));

      await this.audit.log({ action: 'tenant.provision', entityType: 'tenant', entityId: tenant.id, after: { code: tenant.code, plan: plan.code }, tenantId: null }, tx);
      this.logger.log(`Provisioned tenant ${tenant.code} (${tenant.id})`);
      return { tenant, warehouse, adminUserId: admin.id };
    });
  }

  async createSystemRoles(tenantId: string, tx: Tx): Promise<Record<string, string>> {
    const rows = await tx
      .insert(roles)
      .values(SYSTEM_ROLES.map((r) => ({ tenantId, code: r.code, name: r.name, description: r.description, permissions: [...r.permissions], isSystem: true })))
      .returning({ id: roles.id, code: roles.code });
    return Object.fromEntries(rows.map((r) => [r.code, r.id]));
  }

  /** Warehouse with the standard system zones and locations every operation needs. */
  async createWarehouse(tenantId: string, code: string, name: string, timezone: string, tx: Tx) {
    const [wh] = await tx.insert(warehouses).values({ tenantId, code, name, timezone }).returning();

    const zoneDefs = [
      { code: 'RCV', name: 'Receiving Docks', type: 'RECEIVING_DOCK', seq: 10 },
      { code: 'SHP', name: 'Shipping Docks', type: 'SHIPPING_DOCK', seq: 900 },
      { code: 'STG', name: 'Staging', type: 'STAGING', seq: 800 },
      { code: 'PCK', name: 'Packing', type: 'PACKING', seq: 850 },
      { code: 'QC', name: 'Quality Control', type: 'QC', seq: 20 },
      { code: 'QRT', name: 'Quarantine', type: 'QUARANTINE', seq: 30 },
      { code: 'DMG', name: 'Damaged Goods', type: 'DAMAGED', seq: 40 },
      { code: 'RET', name: 'Returns', type: 'RETURNS', seq: 50 },
      { code: 'AMB', name: 'Ambient Storage', type: 'STORAGE', seq: 100 },
    ] as const;
    const zoneRows = await tx
      .insert(zones)
      .values(zoneDefs.map((z) => ({ tenantId, warehouseId: wh.id, code: z.code, name: z.name, type: z.type, pickSequence: z.seq })))
      .returning({ id: zones.id, code: zones.code });
    const zid = Object.fromEntries(zoneRows.map((z) => [z.code, z.id]));

    const locDefs = [
      { code: 'DOCK-01', zone: 'RCV', type: 'DOCK', usage: 'DOCK' },
      { code: 'DOCK-02', zone: 'RCV', type: 'DOCK', usage: 'DOCK' },
      { code: 'SHIP-01', zone: 'SHP', type: 'DOCK', usage: 'DOCK' },
      { code: 'STAGE-01', zone: 'STG', type: 'STAGE', usage: 'STAGING' },
      { code: 'STAGE-02', zone: 'STG', type: 'STAGE', usage: 'STAGING' },
      { code: 'PACK-01', zone: 'PCK', type: 'PACK_STATION', usage: 'PACKING' },
      { code: 'QC-01', zone: 'QC', type: 'FLOOR', usage: 'QC' },
      { code: 'QRT-01', zone: 'QRT', type: 'FLOOR', usage: 'QUARANTINE' },
      { code: 'DMG-01', zone: 'DMG', type: 'FLOOR', usage: 'DAMAGED' },
      { code: 'RET-01', zone: 'RET', type: 'FLOOR', usage: 'RETURNS' },
      { code: 'LOST-FOUND', zone: 'QC', type: 'VIRTUAL', usage: 'QC' },
    ] as const;
    const locRows = await tx
      .insert(locations)
      .values(locDefs.map((l) => ({ tenantId, warehouseId: wh.id, zoneId: zid[l.zone], code: l.code, barcode: l.code, type: l.type, usage: l.usage, allowMixedSku: true, allowMixedLots: true })))
      .returning({ id: locations.id, code: locations.code });
    const lid = Object.fromEntries(locRows.map((l) => [l.code, l.id]));

    const settings = {
      receivingTolerancePct: 0,
      requireLpnOnReceipt: true,
      autoCreatePutawayTasks: true,
      defaultReceivingLocationId: lid['DOCK-01'],
      defaultStagingLocationId: lid['STAGE-01'],
      defaultPackingLocationId: lid['PACK-01'],
      lostAndFoundLocationId: lid['LOST-FOUND'],
      pickConfirmationMode: 'SCAN_LOCATION_AND_SKU',
      allowShortPick: true,
      requireGatePass: true,
      dayEndHour: 23,
    };
    const [updated] = await tx.update(warehouses).set({ settings }).where(eq(warehouses.id, wh.id)).returning();
    return updated;
  }
}
