import { Injectable } from '@nestjs/common';
import * as argon2 from 'argon2';
import { and, desc, eq, gte, lte, sql } from 'drizzle-orm';
import { CreatePlanDto, UpdateTenantDto, UsageMetric } from '@wms/shared';
import { z } from 'zod';
import { UpdatePlanSchema, UpdateSubscriptionSchema, SetFeatureFlagSchema } from '@wms/shared';
import { DbService, Tx } from '@/common/db/db.service';
import { plans, tenants, subscriptions, featureFlags, usageRecords, platformUsers, users, warehouses, storers } from '@/db/schema';
import { AuditService } from '@/common/audit/audit.service';
import { ApiUsageService } from '@/common/auth/api-usage.service';
import { NotFoundError, DomainError } from '@/common/errors/domain-error';
import { D, serialize, qcol } from '@/common/util';

/** Platform-level operations: plans, tenants, subscriptions, feature flags, usage. */
@Injectable()
export class PlatformService {
  constructor(
    private readonly db: DbService,
    private readonly audit: AuditService,
    private readonly apiUsage: ApiUsageService,
  ) {}

  // ---- plans ----
  listPlans() {
    return this.db.runBypass((tx) => tx.select().from(plans).orderBy(plans.monthlyPrice)).then(serialize);
  }

  createPlan(dto: CreatePlanDto) {
    return this.db.runBypass(async (tx) => {
      const [row] = await tx.insert(plans).values({ ...dto, monthlyPrice: D(dto.monthlyPrice) }).returning();
      await this.audit.log({ action: 'plan.create', entityType: 'plan', entityId: row.id, after: dto, tenantId: null }, tx);
      return serialize(row);
    });
  }

  updatePlan(id: string, dto: z.infer<typeof UpdatePlanSchema>) {
    return this.db.runBypass(async (tx) => {
      const [before] = await tx.select().from(plans).where(eq(plans.id, id)).limit(1);
      if (!before) throw new NotFoundError('Plan', id);
      const [row] = await tx
        .update(plans)
        .set({ ...dto, monthlyPrice: dto.monthlyPrice !== undefined ? D(dto.monthlyPrice) : undefined })
        .where(eq(plans.id, id))
        .returning();
      await this.audit.log({ action: 'plan.update', entityType: 'plan', entityId: id, before, after: row, tenantId: null }, tx);
      return serialize(row);
    });
  }

  // ---- tenants ----
  async listTenants() {
    return this.db.runBypass(async (tx) => {
      const rows = await tx
        .select({
          id: tenants.id,
          code: tenants.code,
          name: tenants.name,
          status: tenants.status,
          currency: tenants.currency,
          timezone: tenants.timezone,
          createdAt: tenants.createdAt,
          userCount: sql<number>`(select count(*) from ${users} u where u.tenant_id = ${qcol(tenants.id)})`.mapWith(Number),
          warehouseCount: sql<number>`(select count(*) from ${warehouses} w where w.tenant_id = ${qcol(tenants.id)})`.mapWith(Number),
          storerCount: sql<number>`(select count(*) from ${storers} s where s.tenant_id = ${qcol(tenants.id)})`.mapWith(Number),
        })
        .from(tenants)
        .orderBy(desc(tenants.createdAt));
      return rows;
    });
  }

  async getTenant(id: string) {
    return this.db.runBypass(async (tx) => {
      const [t] = await tx.select().from(tenants).where(eq(tenants.id, id)).limit(1);
      if (!t) throw new NotFoundError('Tenant', id);
      const subs = await tx
        .select({ sub: subscriptions, plan: plans })
        .from(subscriptions)
        .innerJoin(plans, eq(plans.id, subscriptions.planId))
        .where(eq(subscriptions.tenantId, id))
        .orderBy(desc(subscriptions.createdAt));
      const flags = await tx.select().from(featureFlags).where(eq(featureFlags.tenantId, id));
      return serialize({ ...t, subscriptions: subs, featureFlags: flags });
    });
  }

  updateTenant(id: string, dto: UpdateTenantDto) {
    return this.db.runBypass(async (tx) => {
      const [before] = await tx.select().from(tenants).where(eq(tenants.id, id)).limit(1);
      if (!before) throw new NotFoundError('Tenant', id);
      const [row] = await tx.update(tenants).set(dto).where(eq(tenants.id, id)).returning();
      await this.audit.log({ action: 'tenant.update', entityType: 'tenant', entityId: id, before, after: row, tenantId: null }, tx);
      return row;
    });
  }

  updateSubscription(tenantId: string, dto: z.infer<typeof UpdateSubscriptionSchema>) {
    return this.db.runBypass(async (tx) => {
      const [current] = await tx.select().from(subscriptions).where(eq(subscriptions.tenantId, tenantId)).orderBy(desc(subscriptions.createdAt)).limit(1);
      if (!current) throw new NotFoundError('Subscription');
      let planId = current.planId;
      if (dto.planCode) {
        const [p] = await tx.select().from(plans).where(eq(plans.code, dto.planCode)).limit(1);
        if (!p) throw new NotFoundError('Plan', dto.planCode);
        planId = p.id;
        // sync feature flags to the new plan (adds only; explicit disables remain)
        for (const key of p.features) {
          await tx.insert(featureFlags).values({ tenantId, key, enabled: true }).onConflictDoNothing();
        }
      }
      const [row] = await tx
        .update(subscriptions)
        .set({ planId, status: dto.status, endsAt: dto.endsAt === undefined ? undefined : dto.endsAt ? new Date(dto.endsAt) : null, seats: dto.seats, notes: dto.notes })
        .where(eq(subscriptions.id, current.id))
        .returning();
      if (dto.status === 'ACTIVE') await tx.update(tenants).set({ status: 'ACTIVE' }).where(eq(tenants.id, tenantId));
      await this.audit.log({ action: 'subscription.update', entityType: 'subscription', entityId: row.id, before: current, after: row, tenantId: null }, tx);
      this.apiUsage.invalidate(tenantId);
      return row;
    });
  }

  setFeatureFlag(tenantId: string, dto: z.infer<typeof SetFeatureFlagSchema>) {
    return this.db.runBypass(async (tx) => {
      const [row] = await tx
        .insert(featureFlags)
        .values({ tenantId, key: dto.key, enabled: dto.enabled, config: dto.config })
        .onConflictDoUpdate({ target: [featureFlags.tenantId, featureFlags.key], set: { enabled: dto.enabled, config: dto.config } })
        .returning();
      await this.audit.log({ action: 'feature_flag.set', entityType: 'feature_flag', entityId: row.id, after: dto, tenantId: null }, tx);
      return row;
    });
  }

  // ---- usage metering ----
  /** Adds to a daily usage counter. Called from domain event handlers; idempotent on (tenant, metric, date) upsert. */
  async recordUsage(tenantId: string, metric: UsageMetric, quantity: number, dateIso: string, tx: Tx) {
    await tx
      .insert(usageRecords)
      .values({ tenantId, metric, usageDate: dateIso, quantity: D(quantity) })
      .onConflictDoUpdate({ target: [usageRecords.tenantId, usageRecords.metric, usageRecords.usageDate], set: { quantity: sql`${usageRecords.quantity} + ${D(quantity).toFixed(4)}` } });
  }

  usage(q: { tenantId?: string; from: string; to: string; metric?: UsageMetric }) {
    return this.db.runBypass(async (tx) => {
      const where = [gte(usageRecords.usageDate, q.from), lte(usageRecords.usageDate, q.to)];
      if (q.tenantId) where.push(eq(usageRecords.tenantId, q.tenantId));
      if (q.metric) where.push(eq(usageRecords.metric, q.metric));
      const rows = await tx.select().from(usageRecords).where(and(...where)).orderBy(usageRecords.usageDate);
      return serialize(rows);
    });
  }

  /** Current plan limits for a tenant (latest subscription). Null when the tenant has no subscription. */
  async planLimits(tenantId: string, tx: Tx): Promise<Record<string, number> | null> {
    const [row] = await tx
      .select({ limits: plans.limits })
      .from(subscriptions)
      .innerJoin(plans, eq(plans.id, subscriptions.planId))
      .where(eq(subscriptions.tenantId, tenantId))
      .orderBy(desc(subscriptions.createdAt))
      .limit(1);
    return (row?.limits as Record<string, number> | undefined) ?? null;
  }

  /** Sum of a usage metric for a tenant between two dates (inclusive). */
  async usageTotal(tenantId: string, metric: UsageMetric, from: string, to: string, tx: Tx): Promise<number> {
    const [row] = await tx
      .select({ total: sql<string>`coalesce(sum(${usageRecords.quantity}),0)` })
      .from(usageRecords)
      .where(and(eq(usageRecords.tenantId, tenantId), eq(usageRecords.metric, metric), gte(usageRecords.usageDate, from), lte(usageRecords.usageDate, to)));
    return Number(row?.total ?? 0);
  }

  /** Plan limit check used by provisioning-sensitive operations (create user / warehouse / storer). */
  async assertWithinLimit(tenantId: string, limitKey: 'maxWarehouses' | 'maxUsers' | 'maxStorers', currentCount: number, tx: Tx) {
    const [row] = await tx
      .select({ limits: plans.limits })
      .from(subscriptions)
      .innerJoin(plans, eq(plans.id, subscriptions.planId))
      .where(eq(subscriptions.tenantId, tenantId))
      .orderBy(desc(subscriptions.createdAt))
      .limit(1);
    const limit = row?.limits?.[limitKey];
    if (limit !== undefined && currentCount >= Number(limit)) {
      throw new DomainError('PLAN_LIMIT', `Plan limit reached for ${limitKey} (${limit}). Upgrade the subscription to add more.`);
    }
  }

  // ---- platform users ----
  async ensureBootstrapAdmin(email: string, password: string) {
    await this.db.runBypass(async (tx) => {
      const [existing] = await tx.select({ id: platformUsers.id }).from(platformUsers).where(eq(platformUsers.email, email.toLowerCase())).limit(1);
      if (existing) return;
      await tx.insert(platformUsers).values({ email: email.toLowerCase(), passwordHash: await argon2.hash(password), name: 'Platform Admin', role: 'OWNER' });
    });
  }
}
