import { Injectable, NotFoundException, BadRequestException, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { NatsService } from '../../services/nats.service';
import { GameInstance } from '../../entities/game-instance.entity';

/** Lifecycle funcs the agent's {instance}.cmd handler accepts. */
const LIFECYCLE_FUNCS = ['start', 'stop', 'restart', 'status', 'steam_update'] as const;
export type LifecycleFunc = (typeof LIFECYCLE_FUNCS)[number];

@Injectable()
export class InstancesService {
  private readonly logger = new Logger(InstancesService.name);

  constructor(
    private readonly nats: NatsService,
    @InjectRepository(GameInstance)
    private readonly instanceRepo: Repository<GameInstance>,
  ) {}

  /** Resolve an instance the caller's license actually owns (tenant guard). */
  private async resolveInstance(licenseId: string, instanceId: string): Promise<GameInstance> {
    const inst = await this.instanceRepo.findOne({
      where: { id: instanceId, license_id: licenseId },
    });
    if (!inst) throw new NotFoundException('Instance not found');
    return inst;
  }

  async lifecycle(licenseId: string, instanceId: string, func: LifecycleFunc): Promise<unknown> {
    if (!LIFECYCLE_FUNCS.includes(func)) {
      throw new BadRequestException(`Unsupported action '${func}'`);
    }
    const inst = await this.resolveInstance(licenseId, instanceId);
    const subject = `corrosion.${licenseId}.${inst.agent_instance_id}.cmd`;
    this.logger.log(`instance ${inst.agent_instance_id}: ${func}`);
    return this.nats.requestScoped(licenseId, subject, { func });
  }

  async rcon(licenseId: string, instanceId: string, command: string): Promise<unknown> {
    if (!command || !command.trim()) {
      throw new BadRequestException('command is required');
    }
    const inst = await this.resolveInstance(licenseId, instanceId);
    const subject = `corrosion.${licenseId}.${inst.agent_instance_id}.cmd`;
    // RCON can take longer than a lifecycle ack — give it more headroom.
    return this.nats.requestScoped(licenseId, subject, { func: 'rcon', command }, 12_000);
  }

  // -------------------------------------------------------------------------
  // File access — jailed to the instance root by the agent's file manager.
  // The agent protocol (corrosion-host-agent/src/filemanager.rs):
  //   { op: list|read|write|delete|rename|mkdir|mkfile|move|copy, path, ... }
  //   reply: { status: 'success'|'error', data?, message? }
  // -------------------------------------------------------------------------

  private filesSubject(inst: GameInstance, licenseId: string): string {
    return `corrosion.${licenseId}.${inst.agent_instance_id}.files.cmd`;
  }

  private async fileOp(
    licenseId: string,
    instanceId: string,
    payload: Record<string, unknown>,
  ): Promise<{ status: string; data?: unknown; message?: string }> {
    const inst = await this.resolveInstance(licenseId, instanceId);
    const res = await this.nats.requestScoped<{ status: string; data?: unknown; message?: string }>(
      licenseId,
      this.filesSubject(inst, licenseId),
      payload,
      12_000,
    );
    if (res?.status === 'error') {
      throw new BadRequestException(res.message ?? 'File operation failed');
    }
    return res;
  }

  async listFiles(licenseId: string, instanceId: string, path = ''): Promise<unknown> {
    const res = await this.fileOp(licenseId, instanceId, { op: 'list', path });
    return res.data;
  }

  async readFile(licenseId: string, instanceId: string, path: string): Promise<unknown> {
    if (!path) throw new BadRequestException('path is required');
    const res = await this.fileOp(licenseId, instanceId, { op: 'read', path });
    return res.data;
  }

  async writeFile(
    licenseId: string,
    instanceId: string,
    path: string,
    content: string,
  ): Promise<unknown> {
    if (!path) throw new BadRequestException('path is required');
    const res = await this.fileOp(licenseId, instanceId, { op: 'write', path, content });
    return res.data ?? { status: 'success' };
  }

  async deleteFile(licenseId: string, instanceId: string, path: string): Promise<unknown> {
    if (!path) throw new BadRequestException('path is required');
    return (await this.fileOp(licenseId, instanceId, { op: 'delete', path })).data ?? { ok: true };
  }

  async renameFile(
    licenseId: string,
    instanceId: string,
    path: string,
    name: string,
  ): Promise<unknown> {
    if (!path || !name) throw new BadRequestException('path and name are required');
    return (await this.fileOp(licenseId, instanceId, { op: 'rename', path, name })).data ?? { ok: true };
  }

  async mkdir(licenseId: string, instanceId: string, path: string): Promise<unknown> {
    if (!path) throw new BadRequestException('path is required');
    return (await this.fileOp(licenseId, instanceId, { op: 'mkdir', path })).data ?? { ok: true };
  }

  async mkfile(licenseId: string, instanceId: string, path: string): Promise<unknown> {
    if (!path) throw new BadRequestException('path is required');
    return (await this.fileOp(licenseId, instanceId, { op: 'mkfile', path })).data ?? { ok: true };
  }

  async moveFile(
    licenseId: string,
    instanceId: string,
    path: string,
    dest: string,
  ): Promise<unknown> {
    if (!path || !dest) throw new BadRequestException('path and dest are required');
    return (await this.fileOp(licenseId, instanceId, { op: 'move', path, dest })).data ?? { ok: true };
  }

  async copyFile(
    licenseId: string,
    instanceId: string,
    path: string,
    dest: string,
  ): Promise<unknown> {
    if (!path || !dest) throw new BadRequestException('path and dest are required');
    return (await this.fileOp(licenseId, instanceId, { op: 'copy', path, dest })).data ?? { ok: true };
  }

  /**
   * Wipe an instance's game data via the agent's jailed wipe handler: stop →
   * delete files per wipe_type (map/blueprint/full) → restart. Long timeout
   * because the agent does all three steps before replying.
   */
  async wipe(
    licenseId: string,
    instanceId: string,
    wipeType: 'map' | 'blueprint' | 'full',
    backup = true,
  ): Promise<unknown> {
    const inst = await this.resolveInstance(licenseId, instanceId);
    const subject = `corrosion.${licenseId}.${inst.agent_instance_id}.cmd`;
    this.logger.log(`instance ${inst.agent_instance_id}: wipe (${wipeType})`);
    return this.nats.requestScoped(
      licenseId,
      subject,
      { func: 'wipe', wipe_type: wipeType, backup },
      120_000,
    );
  }

  // -------------------------------------------------------------------------
  // License-scoped convenience wrappers. Legacy single-server services
  // (servers/players/schedules/wipes/plugins + the 9 plugin-config modules)
  // predate the instance model and carry only a licenseId. These resolve the
  // license's primary instance, then dispatch to the agent — replacing the old
  // publishes to the now-defunct `cmd.server` subject.
  // -------------------------------------------------------------------------

  /** The license's primary (oldest) instance. Throws if none is connected. */
  async resolveDefaultInstance(licenseId: string): Promise<GameInstance> {
    const inst = await this.instanceRepo.findOne({
      where: { license_id: licenseId },
      order: { created_at: 'ASC' },
    });
    if (!inst) {
      throw new NotFoundException(
        'No game instance is connected for this license yet — install and start the host agent first.',
      );
    }
    return inst;
  }

  async lifecycleForLicense(licenseId: string, func: LifecycleFunc): Promise<unknown> {
    const inst = await this.resolveDefaultInstance(licenseId);
    return this.lifecycle(licenseId, inst.id, func);
  }

  async rconForLicense(licenseId: string, command: string): Promise<unknown> {
    const inst = await this.resolveDefaultInstance(licenseId);
    return this.rcon(licenseId, inst.id, command);
  }

  async writeFileForLicense(licenseId: string, path: string, content: string): Promise<unknown> {
    const inst = await this.resolveDefaultInstance(licenseId);
    return this.writeFile(licenseId, inst.id, path, content);
  }

  async readFileForLicense(licenseId: string, path: string): Promise<unknown> {
    const inst = await this.resolveDefaultInstance(licenseId);
    return this.readFile(licenseId, inst.id, path);
  }

  async deleteFileForLicense(licenseId: string, path: string): Promise<unknown> {
    const inst = await this.resolveDefaultInstance(licenseId);
    return this.deleteFile(licenseId, inst.id, path);
  }

  async wipeForLicense(
    licenseId: string,
    wipeType: 'map' | 'blueprint' | 'full',
    backup = true,
  ): Promise<unknown> {
    const inst = await this.resolveDefaultInstance(licenseId);
    return this.wipe(licenseId, inst.id, wipeType, backup);
  }
}
