import {
  Injectable,
  NotFoundException,
  Logger,
  OnModuleInit,
  OnModuleDestroy,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { IsNull, LessThanOrEqual, Repository } from 'typeorm';
import { WipeProfile } from '../../entities/wipe-profile.entity';
import { WipeSchedule } from '../../entities/wipe-schedule.entity';
import { WipeHistory } from '../../entities/wipe-history.entity';
import { CreateProfileDto } from './dto/create-profile.dto';
import { UpdateProfileDto } from './dto/update-profile.dto';
import { CreateScheduleDto } from './dto/create-schedule.dto';
import { TriggerWipeDto } from './dto/trigger-wipe.dto';
import { InstancesService } from '../instances/instances.service';
import { WebhooksService } from '../webhooks/webhooks.service';
import { nextCronDate } from '../../common/cron.util';

@Injectable()
export class WipesService implements OnModuleInit, OnModuleDestroy {
  private readonly logger = new Logger(WipesService.name);
  private wipeExecutorInterval: ReturnType<typeof setInterval> | null = null;

  constructor(
    @InjectRepository(WipeProfile)
    private readonly wipeProfileRepo: Repository<WipeProfile>,
    @InjectRepository(WipeSchedule)
    private readonly wipeScheduleRepo: Repository<WipeSchedule>,
    @InjectRepository(WipeHistory)
    private readonly wipeHistoryRepo: Repository<WipeHistory>,
    private readonly instancesService: InstancesService,
    private readonly webhooksService: WebhooksService,
  ) {}

  // ---------------------------------------------------------------------------
  // Scheduled-wipe executor — the auto-wiper. Mirrors SchedulesService: a 60s
  // poll fires every active wipe schedule whose next_scheduled_run is due, then
  // advances it from its cron expression. Without this, wipe_schedules rows
  // never fire (the headline auto-wipe feature was inert).
  // ---------------------------------------------------------------------------

  onModuleInit(): void {
    this.bootstrapWipeSchedules().catch((err) =>
      this.logger.error('Failed to bootstrap wipe-schedule next runs', err),
    );
    this.wipeExecutorInterval = setInterval(() => {
      this.executeDueWipes().catch((err) =>
        this.logger.error('Wipe-schedule executor error', err),
      );
    }, 60_000);
    this.logger.log('Wipe-schedule executor started (60s polling interval)');
  }

  onModuleDestroy(): void {
    if (this.wipeExecutorInterval) {
      clearInterval(this.wipeExecutorInterval);
      this.wipeExecutorInterval = null;
    }
  }

  /** On startup, stamp next_scheduled_run on active schedules that lack one. */
  private async bootstrapWipeSchedules(): Promise<void> {
    const schedules = await this.wipeScheduleRepo.find({
      where: { is_active: true, next_scheduled_run: IsNull() },
    });
    for (const s of schedules) {
      const next = nextCronDate(s.cron_expression, new Date());
      if (next) {
        s.next_scheduled_run = next;
        await this.wipeScheduleRepo.save(s);
      }
    }
    if (schedules.length > 0) {
      this.logger.log(`Bootstrapped next run for ${schedules.length} wipe schedule(s)`);
    }
  }

  /** Fire every active wipe schedule whose next_scheduled_run <= now. */
  private async executeDueWipes(): Promise<void> {
    const now = new Date();
    const due = await this.wipeScheduleRepo.find({
      where: { is_active: true, next_scheduled_run: LessThanOrEqual(now) },
    });
    if (due.length === 0) return;

    this.logger.log(`Executing ${due.length} due wipe schedule(s)`);
    for (const s of due) {
      try {
        await this.triggerWipe(
          s.license_id,
          {
            wipe_type: s.wipe_type as TriggerWipeDto['wipe_type'],
            wipe_profile_id: s.wipe_profile_id,
          },
          'scheduled',
        );
      } catch (err) {
        this.logger.error(
          `Scheduled wipe failed for schedule ${s.id} (${s.schedule_name})`,
          (err as Error).stack,
        );
      } finally {
        // Advance next_scheduled_run regardless, so a failing schedule doesn't
        // re-fire every 60s.
        s.next_scheduled_run = nextCronDate(s.cron_expression, now);
        await this.wipeScheduleRepo.save(s);
      }
    }
  }

  async getProfiles(licenseId: string): Promise<WipeProfile[]> {
    return this.wipeProfileRepo.find({
      where: { license_id: licenseId },
      order: { created_at: 'DESC' },
    });
  }

  async createProfile(licenseId: string, dto: CreateProfileDto): Promise<WipeProfile> {
    const profile = this.wipeProfileRepo.create({
      license_id: licenseId,
      ...dto,
    });
    return this.wipeProfileRepo.save(profile);
  }

  async updateProfile(
    licenseId: string,
    profileId: string,
    dto: UpdateProfileDto,
  ): Promise<WipeProfile> {
    const profile = await this.wipeProfileRepo.findOne({
      where: { id: profileId, license_id: licenseId },
    });

    if (!profile) {
      throw new NotFoundException(`Wipe profile ${profileId} not found`);
    }

    Object.assign(profile, dto);
    profile.updated_at = new Date();
    return this.wipeProfileRepo.save(profile);
  }

  async deleteProfile(licenseId: string, profileId: string): Promise<void> {
    const result = await this.wipeProfileRepo.delete({
      id: profileId,
      license_id: licenseId,
    });

    if (result.affected === 0) {
      throw new NotFoundException(`Wipe profile ${profileId} not found`);
    }
  }

  async getSchedules(licenseId: string): Promise<WipeSchedule[]> {
    return this.wipeScheduleRepo.find({
      where: { license_id: licenseId },
      relations: ['wipe_profile'],
      order: { created_at: 'DESC' },
    });
  }

  async createSchedule(licenseId: string, dto: CreateScheduleDto): Promise<WipeSchedule> {
    const schedule = this.wipeScheduleRepo.create({
      license_id: licenseId,
      ...dto,
    });
    return this.wipeScheduleRepo.save(schedule);
  }

  async getHistory(licenseId: string, limit: number = 50): Promise<WipeHistory[]> {
    return this.wipeHistoryRepo.find({
      where: { license_id: licenseId },
      relations: ['wipe_profile', 'wipe_schedule', 'map'],
      order: { created_at: 'DESC' },
      take: limit,
    });
  }

  async triggerWipe(
    licenseId: string,
    dto: TriggerWipeDto,
    triggerType: 'manual' | 'scheduled' = 'manual',
  ): Promise<{ wipe_history_id: string }> {
    const history = this.wipeHistoryRepo.create({
      license_id: licenseId,
      wipe_type: dto.wipe_type,
      wipe_profile_id: dto.wipe_profile_id,
      trigger_type: triggerType,
      status: 'wiping',
      started_at: new Date(),
    });

    const saved = await this.wipeHistoryRepo.save(history);
    this.logger.log(
      `Wipe ${triggerType} dispatched for license ${licenseId} — history ${saved.id}`,
    );

    // Dispatch to the agent WITHOUT blocking the caller — a wipe is
    // stop → delete → start and can take a minute+. We record the outcome on
    // wipe_history from the agent's reply and fire the wipe_completed webhook
    // when it lands. Previously the row was created 'pending' and never
    // advanced, so history lied about every wipe.
    void this.instancesService
      .wipeForLicense(licenseId, dto.wipe_type, true)
      .then((reply: unknown) => {
        const r = (reply ?? {}) as { status?: string; message?: string; deleted_count?: number };
        const ok = r.status === 'success';
        saved.status = ok ? 'success' : 'failed';
        saved.completed_at = new Date();
        if (!ok) {
          saved.error_message = r.message ?? 'agent reported wipe failure';
        }
        return this.wipeHistoryRepo.save(saved).then(() => {
          this.logger.log(`Wipe ${saved.id} ${saved.status}`);
          if (ok) {
            void this.webhooksService.dispatch(licenseId, 'wipe_completed', {
              wipe_history_id: saved.id,
              wipe_type: dto.wipe_type,
              trigger_type: triggerType,
              deleted_count: r.deleted_count ?? null,
            });
          }
        });
      })
      .catch((err: unknown) => {
        saved.status = 'failed';
        saved.completed_at = new Date();
        saved.error_message = err instanceof Error ? err.message : 'wipe dispatch failed';
        this.logger.warn(`Wipe ${saved.id} failed: ${saved.error_message}`);
        void this.wipeHistoryRepo.save(saved);
      });

    return { wipe_history_id: saved.id };
  }

  async triggerDryRun(
    licenseId: string,
    dto: TriggerWipeDto,
  ): Promise<{
    would_delete: string[];
    would_preserve: string[];
    estimated_duration_seconds: number;
    profile_name: string | null;
    notes: string[];
  }> {
    // Resolve profile config if a profile ID was supplied.
    let profile: WipeProfile | null = null;
    if (dto.wipe_profile_id) {
      profile = await this.wipeProfileRepo.findOne({
        where: { id: dto.wipe_profile_id, license_id: licenseId },
      });
    }

    if (!profile && dto.wipe_profile_id) {
      throw new NotFoundException(`Wipe profile ${dto.wipe_profile_id} not found`);
    }

    const notes: string[] = [];

    // Base files affected by all wipe types.
    const would_delete: string[] = ['*.map', '*.sav'];
    const would_preserve: string[] = [
      'oxide/',
      'oxide/plugins/',
      'cfg/',
      'server.cfg',
    ];

    // Blueprint wipe additions.
    if (dto.wipe_type === 'blueprint' || dto.wipe_type === 'full') {
      would_delete.push('player.blueprints.db', 'player.tech.db');
    }

    // Full wipe: also clear player data and oxide data.
    if (dto.wipe_type === 'full') {
      would_delete.push(
        'player.deaths.db',
        'player.identities.db',
        'player.states.db',
        'player.tokens.db',
        'oxide/data/*',
      );
      would_preserve.splice(would_preserve.indexOf('oxide/'), 1);
    }

    // Factor in pre_wipe_config from the profile (if set).
    let estimatedSeconds = 45;

    if (profile) {
      const pre = profile.pre_wipe_config as Record<string, any>;
      const post = profile.post_wipe_config as Record<string, any>;

      if (pre?.backup_before_wipe) {
        estimatedSeconds += 60;
        notes.push('Pre-wipe backup will run before deletion (+60s)');
        would_preserve.push('backups/');
      }

      if (pre?.kick_players_before_wipe) {
        const countdownWarnings: number[] = (pre.countdown_warnings as number[]) ?? [];
        const maxWarning = countdownWarnings.length > 0 ? Math.max(...countdownWarnings) : 0;
        if (maxWarning > 0) {
          estimatedSeconds += maxWarning * 60;
          notes.push(`Players will be warned ${countdownWarnings.join(', ')} minutes before kick (+${maxWarning * 60}s)`);
        }
      }

      if (post?.verify_server_started) {
        estimatedSeconds += 30;
        notes.push('Post-wipe: server health check will run (+30s)');
      }

      if (post?.rollback_on_failure) {
        notes.push('Rollback on failure is enabled — backup will be preserved if wipe fails');
      }

      if (post?.max_restart_attempts) {
        const attempts = post.max_restart_attempts as number;
        if (attempts > 1) {
          estimatedSeconds += (attempts - 1) * 15;
          notes.push(`Up to ${attempts} restart attempts (+${(attempts - 1) * 15}s max)`);
        }
      }
    } else {
      notes.push('No profile selected — using default wipe behavior');
    }

    // Account for world size in time estimate.
    // Larger worlds take longer to clear from disk (rough heuristic).
    // We don't have world_size here without querying server_config,
    // so apply a static estimate per wipe type.
    if (dto.wipe_type === 'full') {
      estimatedSeconds += 75;
    } else if (dto.wipe_type === 'blueprint') {
      estimatedSeconds += 10;
    }

    this.logger.log(
      `Dry-run for license ${licenseId}: type=${dto.wipe_type}, ` +
      `profile=${profile?.profile_name ?? 'none'}, estimated=${estimatedSeconds}s`,
    );

    return {
      would_delete,
      would_preserve,
      estimated_duration_seconds: estimatedSeconds,
      profile_name: profile?.profile_name ?? null,
      notes,
    };
  }
}
