import { Injectable, Logger, type OnApplicationBootstrap } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { InjectModel } from '@nestjs/mongoose'
import { Model } from 'mongoose'
import { AuthentikClient } from '../integrations/authentik.client.js'
import { PricesService } from '../prices/prices.service.js'
import { Flag, FlagDocument, type FlagState } from '../schemas/flag.schema.js'
import { Subscription, SubscriptionDocument } from '../schemas/subscription.schema.js'
import { Tenant, TenantDocument } from '../schemas/tenant.schema.js'
import { User, UserDocument } from '../schemas/user.schema.js'

// Stages we wire into the recovery flow. All three ship with Authentik's
// default install — see `ak api /api/v3/stages/all/`. If any is missing
// (custom Authentik build), the bootstrap step logs a warning and skips.
const RECOVERY_FLOW_SLUG = 'default-dezky-recovery'
const RECOVERY_STAGES = [
  { name: 'default-authentication-identification', order: 10 },
  { name: 'default-password-change-prompt',        order: 20 },
  { name: 'default-password-change-write',         order: 30 },
] as const

interface SeedFlag {
  key: string
  description: string
  state: FlagState
  pct?: number
  scope?: {
    plans?: string[]
    tenantSlugs?: string[]
    partnerSlugs?: string[]
    environments?: ('prod' | 'staging' | 'dev')[]
  }
}

// Initial set of flags. Keys + states map to real Dezky concerns documented
// in the operator design — flags here become real switches the operator
// can flip once the gated features land. Seeded with $setOnInsert so an
// operator editing them later doesn't get clobbered on restart.
const FLAG_SEEDS: SeedFlag[] = [
  { key: 'jmap_native_v2',        state: 'off',      description: 'Native JMAP client (replacing the HTTP-stub bridge)',     scope: { plans: ['pro', 'enterprise'] } },
  { key: 'oci_versioning',        state: 'on',       description: 'OCIS file versioning enabled' },
  { key: 'jitsi_recording_e2ee',  state: 'targeted', description: 'E2EE recording in Jitsi (pilot)' },
  { key: 'new_billing_engine',    state: 'off',      description: 'Stripe-backed billing engine — replacing manual invoicing' },
  { key: 'gdpr_export_v2',        state: 'off',      description: 'Self-serve GDPR data export — kept off until reviewed' },
  { key: 'whitelabel_cssprops',   state: 'on',       description: 'Per-partner whitelabel CSS variables' },
  { key: 'audit_log_streaming',   state: 'off',      description: 'Real-time audit log streaming to the operator UI',         scope: { plans: ['enterprise'] } },
  { key: 'zulip_topic_threading', state: 'off',      description: 'Zulip topic threading UI (post deploy)' },
  { key: 'tos_2026_acceptance',   state: 'on',       description: 'Force users to accept the v2026 Terms of Service' },
  { key: 'beta_ai_summaries',     state: 'off',      description: 'AI summaries for inbox + files — killed pending review' },
]

// Idempotent seed: a default 'dezky' tenant + the akadmin user, so the portal can
// query non-empty results immediately. Real user records are bootstrapped via
// UsersController.me() on each user's first authenticated request.
@Injectable()
export class SeedService implements OnApplicationBootstrap {
  private readonly logger = new Logger(SeedService.name)

  constructor(
    @InjectModel(Tenant.name) private readonly tenantModel: Model<TenantDocument>,
    @InjectModel(User.name) private readonly userModel: Model<UserDocument>,
    @InjectModel(Subscription.name) private readonly subModel: Model<SubscriptionDocument>,
    @InjectModel(Flag.name) private readonly flagModel: Model<FlagDocument>,
    private readonly authentik: AuthentikClient,
    private readonly config: ConfigService,
    private readonly prices: PricesService,
  ) {}

  async onApplicationBootstrap(): Promise<void> {
    if (this.config.get('SEED_ENABLED') === 'false') {
      this.logger.log('SEED_ENABLED=false — skipping seed')
      return
    }

    const tenant = await this.tenantModel
      .findOneAndUpdate(
        { slug: 'dezky' },
        {
          $setOnInsert: {
            slug: 'dezky',
            name: 'Dezky',
            status: 'active',
            plan: 'enterprise',
            domains: ['dezky.local'],
            billingInfo: { companyName: 'Dezky', country: 'DK' },
          },
        },
        { new: true, upsert: true },
      )
      .exec()
    this.logger.log(`Tenant ready: ${tenant.slug} (${tenant._id})`)

    await this.subModel
      .findOneAndUpdate(
        { tenantId: tenant._id },
        {
          $setOnInsert: {
            tenantId: tenant._id,
            plan: 'enterprise',
            status: 'active',
          },
        },
        { upsert: true },
      )
      .exec()
    this.logger.log(`Subscription ready for ${tenant.slug}`)

    // No user seeded here — UsersController.me() upserts akadmin on first call.

    // Pricing catalog defaults. Idempotent — operator edits via /pricing
    // (which marks old rows inactive + inserts new ones) are preserved.
    await this.prices.ensureDefaults()
    this.logger.log('Price catalog defaults ensured')

    // Feature flags. Seeded via $setOnInsert so an operator who later edits a
    // flag's state through the UI doesn't get their change reverted on the
    // next bootstrap.
    let createdFlags = 0
    for (const seed of FLAG_SEEDS) {
      const res = await this.flagModel
        .findOneAndUpdate(
          { key: seed.key },
          {
            $setOnInsert: {
              key: seed.key,
              description: seed.description,
              state: seed.state,
              pct: seed.pct ?? 0,
              scope: {
                plans: seed.scope?.plans ?? [],
                tenantSlugs: seed.scope?.tenantSlugs ?? [],
                partnerSlugs: seed.scope?.partnerSlugs ?? [],
                environments: seed.scope?.environments ?? [],
              },
              history: [{ at: new Date(), action: `seeded · state=${seed.state}` }],
            },
          },
          { upsert: true, new: true, rawResult: true },
        )
        .exec()
      // Mongoose returns the raw mongo result; lastErrorObject.updatedExisting
      // is false when it just inserted.
      if (
        (res as unknown as { lastErrorObject?: { updatedExisting?: boolean } })
          .lastErrorObject?.updatedExisting === false
      ) {
        createdFlags++
      }
    }
    if (createdFlags > 0) {
      this.logger.log(`Seeded ${createdFlags} new flag(s) (of ${FLAG_SEEDS.length})`)
    }

    // Recovery flow — production-grade onboarding path. Without this,
    // UsersService.inviteOperator falls back to setting a temp password
    // (which is fine but means an operator briefly handles plaintext). Once
    // wired, recovery links work and the temp-password path goes dormant.
    await this.ensureRecoveryFlow().catch((err) => {
      this.logger.warn(
        `Could not wire Authentik recovery flow (will retry on next boot): ${
          err instanceof Error ? err.message : String(err)
        }`,
      )
    })
  }

  // Idempotent. Creates a recovery flow (designation='recovery') that reuses
  // Authentik's default identification + password-change + user-write stages,
  // then points the default brand's flow_recovery at it. Safe to run on every
  // bootstrap — the existence checks short-circuit no-op work.
  private async ensureRecoveryFlow(): Promise<void> {
    let flow = await this.authentik.findRecoveryFlow()
    if (!flow) {
      flow = await this.authentik.createFlow({
        name: 'Dezky · Account Recovery',
        slug: RECOVERY_FLOW_SLUG,
        title: 'Reset your Dezky password',
        designation: 'recovery',
      })
      this.logger.log(`Created Authentik recovery flow ${flow.slug} (pk=${flow.pk})`)

      // Bind the stages. Order matters — Authentik plays them in ascending
      // order. The identification stage auto-skips when the user is already
      // pinned by the recovery token, so the link-click path goes straight
      // to the password prompt.
      for (const { name, order } of RECOVERY_STAGES) {
        const stage = await this.authentik.findStageByName(name)
        if (!stage) {
          this.logger.warn(`Recovery flow setup: stage "${name}" missing in Authentik, skipping`)
          continue
        }
        await this.authentik.bindStageToFlow({ target: flow.pk, stage: stage.pk, order })
      }
      this.logger.log(`Bound ${RECOVERY_STAGES.length} stages to recovery flow`)
    }

    const brand = await this.authentik.getDefaultBrand()
    if (!brand) {
      this.logger.warn('No default Authentik brand found — recovery flow not bound')
      return
    }
    if (brand.flow_recovery === flow.pk) {
      return  // already wired
    }
    await this.authentik.setBrandRecoveryFlow(brand.brand_uuid, flow.pk)
  }
}
