/**
 * App Store Manager
 * Fetches, caches, and serves app templates from the remote appstore-apptemplates repo.
 * The remote repo is the single source of truth — no fallback catalog.
 */

import type {
  ICatalog,
  ICatalogApp,
  IAppMeta,
  IAppVersionConfig,
  IMigrationContext,
  IMigrationResult,
  IUpgradeableService,
} from './appstore-types.ts';
import { logger } from '../logging.ts';
import { getErrorMessage } from '../utils/error.ts';
import type { Onebox } from './onebox.ts';
import type { IService } from '../types.ts';

export class AppStoreManager {
  private oneboxRef: Onebox;
  private catalogCache: ICatalog | null = null;
  private lastFetchTime = 0;
  private readonly repoBaseUrl = 'https://code.foss.global/serve.zone/appstore-apptemplates/raw/branch/main';
  private readonly cacheTtlMs = 5 * 60 * 1000; // 5 minutes

  constructor(oneboxRef: Onebox) {
    this.oneboxRef = oneboxRef;
  }

  async init(): Promise<void> {
    try {
      await this.getCatalog();
      logger.info(`App Store initialized with ${this.catalogCache?.apps.length || 0} templates`);
    } catch (error) {
      logger.warn(`App Store initialization failed: ${getErrorMessage(error)}`);
      logger.warn('App Store will retry on next request');
    }
  }

  /**
   * Get the catalog (cached, refreshes after TTL)
   */
  async getCatalog(): Promise<ICatalog> {
    const now = Date.now();
    if (this.catalogCache && (now - this.lastFetchTime) < this.cacheTtlMs) {
      return this.catalogCache;
    }

    try {
      const catalog = await this.fetchJson('catalog.json') as ICatalog;
      if (catalog && catalog.apps && Array.isArray(catalog.apps)) {
        this.catalogCache = catalog;
        this.lastFetchTime = now;
        return catalog;
      }
      throw new Error('Invalid catalog format');
    } catch (error) {
      logger.warn(`Failed to fetch remote catalog: ${getErrorMessage(error)}`);
      // Return cached if available, otherwise return empty catalog
      if (this.catalogCache) {
        return this.catalogCache;
      }
      return { schemaVersion: 1, updatedAt: '', apps: [] };
    }
  }

  /**
   * Get the catalog apps list (convenience method for the API)
   */
  async getApps(): Promise<ICatalogApp[]> {
    const catalog = await this.getCatalog();
    return catalog.apps;
  }

  /**
   * Fetch app metadata (versions list, etc.)
   */
  async getAppMeta(appId: string): Promise<IAppMeta> {
    try {
      return await this.fetchJson(`apps/${appId}/app.json`) as IAppMeta;
    } catch (error) {
      throw new Error(`Failed to fetch metadata for app '${appId}': ${getErrorMessage(error)}`);
    }
  }

  /**
   * Fetch full config for an app version
   */
  async getAppVersionConfig(appId: string, version: string): Promise<IAppVersionConfig> {
    try {
      return await this.fetchJson(`apps/${appId}/versions/${version}/config.json`) as IAppVersionConfig;
    } catch (error) {
      throw new Error(`Failed to fetch config for ${appId}@${version}: ${getErrorMessage(error)}`);
    }
  }

  /**
   * Compare deployed services against catalog to find those with available upgrades
   */
  async getUpgradeableServices(): Promise<IUpgradeableService[]> {
    const catalog = await this.getCatalog();
    const services = this.oneboxRef.database.getAllServices();
    const upgradeable: IUpgradeableService[] = [];

    for (const service of services) {
      if (!service.appTemplateId || !service.appTemplateVersion) continue;

      const catalogApp = catalog.apps.find(a => a.id === service.appTemplateId);
      if (!catalogApp) continue;

      if (catalogApp.latestVersion !== service.appTemplateVersion) {
        // Check if a migration script exists
        const hasMigration = await this.hasMigrationScript(
          service.appTemplateId,
          service.appTemplateVersion,
          catalogApp.latestVersion,
        );

        upgradeable.push({
          serviceName: service.name,
          appTemplateId: service.appTemplateId,
          currentVersion: service.appTemplateVersion,
          latestVersion: catalogApp.latestVersion,
          hasMigration,
        });
      }
    }

    return upgradeable;
  }

  /**
   * Check if a migration script exists for a specific version transition
   */
  async hasMigrationScript(appId: string, fromVersion: string, toVersion: string): Promise<boolean> {
    try {
      const scriptPath = `apps/${appId}/versions/${toVersion}/migrate-from-${fromVersion}.ts`;
      await this.fetchText(scriptPath);
      return true;
    } catch {
      return false;
    }
  }

  /**
   * Execute a migration in a sandboxed Deno child process
   */
  async executeMigration(service: IService, fromVersion: string, toVersion: string): Promise<IMigrationResult> {
    const appId = service.appTemplateId;
    if (!appId) {
      throw new Error('Service has no appTemplateId');
    }

    // Fetch the migration script
    const scriptPath = `apps/${appId}/versions/${toVersion}/migrate-from-${fromVersion}.ts`;
    let scriptContent: string;
    try {
      scriptContent = await this.fetchText(scriptPath);
    } catch {
      // No migration script — do a simple config-based upgrade
      logger.info(`No migration script for ${appId} ${fromVersion} -> ${toVersion}, using config-only upgrade`);
      const config = await this.getAppVersionConfig(appId, toVersion);
      return {
        success: true,
        image: config.image,
        envVars: undefined, // Keep existing env vars
        warnings: [],
      };
    }

    // Write to temp file
    const tempFile = `/tmp/onebox-migration-${crypto.randomUUID()}.ts`;
    await Deno.writeTextFile(tempFile, scriptContent);

    try {
      // Prepare context
      const context: IMigrationContext = {
        service: {
          name: service.name,
          image: service.image,
          envVars: service.envVars,
          port: service.port,
        },
        fromVersion,
        toVersion,
      };

      // Execute in sandboxed Deno child process
      const cmd = new Deno.Command('deno', {
        args: ['run', '--allow-env', '--allow-net=none', '--allow-read=none', '--allow-write=none', tempFile],
        stdin: 'piped',
        stdout: 'piped',
        stderr: 'piped',
      });

      const child = cmd.spawn();

      // Write context to stdin
      const writer = child.stdin.getWriter();
      await writer.write(new TextEncoder().encode(JSON.stringify(context)));
      await writer.close();

      // Read result
      const output = await child.output();
      const exitCode = output.code;
      const stdout = new TextDecoder().decode(output.stdout);
      const stderr = new TextDecoder().decode(output.stderr);

      if (exitCode !== 0) {
        logger.error(`Migration script failed (exit ${exitCode}): ${stderr.substring(0, 500)}`);
        return {
          success: false,
          warnings: [`Migration script failed: ${stderr.substring(0, 200)}`],
        };
      }

      // Parse result from stdout
      try {
        const result = JSON.parse(stdout) as IMigrationResult;
        result.success = true;
        return result;
      } catch {
        logger.error(`Failed to parse migration output: ${stdout.substring(0, 200)}`);
        return {
          success: false,
          warnings: ['Migration script produced invalid output'],
        };
      }
    } finally {
      // Cleanup temp file
      try {
        await Deno.remove(tempFile);
      } catch {
        // Ignore cleanup errors
      }
    }
  }

  /**
   * Apply an upgrade: update image, env vars, recreate container
   */
  async applyUpgrade(
    serviceName: string,
    migrationResult: IMigrationResult,
    newVersion: string,
  ): Promise<IService> {
    const service = this.oneboxRef.database.getServiceByName(serviceName);
    if (!service) {
      throw new Error(`Service not found: ${serviceName}`);
    }

    // Stop the existing container
    if (service.containerID && service.status === 'running') {
      await this.oneboxRef.services.stopService(serviceName);
    }

    // Update service record
    const updates: Partial<IService> = {
      appTemplateVersion: newVersion,
    };

    if (migrationResult.image) {
      updates.image = migrationResult.image;
    }

    if (migrationResult.envVars) {
      // Merge: migration result provides base, user overrides preserved
      const mergedEnvVars = { ...migrationResult.envVars };
      // Keep any user-set env vars that aren't in the migration result
      for (const [key, value] of Object.entries(service.envVars)) {
        if (!(key in mergedEnvVars)) {
          mergedEnvVars[key] = value;
        }
      }
      updates.envVars = mergedEnvVars;
    }

    this.oneboxRef.database.updateService(service.id!, updates);

    // Pull new image if changed
    const newImage = migrationResult.image || service.image;
    if (migrationResult.image && migrationResult.image !== service.image) {
      await this.oneboxRef.docker.pullImage(newImage);
    }

    // Recreate and start container
    const updatedService = this.oneboxRef.database.getServiceByName(serviceName)!;

    // Remove old container
    if (service.containerID) {
      try {
        await this.oneboxRef.docker.removeContainer(service.containerID, true);
      } catch {
        // Container might already be gone
      }
    }

    // Create new container
    const containerID = await this.oneboxRef.docker.createContainer(updatedService);
    this.oneboxRef.database.updateService(service.id!, { containerID, status: 'starting' });

    // Start container
    await this.oneboxRef.docker.startContainer(containerID);
    this.oneboxRef.database.updateService(service.id!, { status: 'running' });

    logger.success(`Service '${serviceName}' upgraded to template version ${newVersion}`);
    return this.oneboxRef.database.getServiceByName(serviceName)!;
  }

  /**
   * Fetch JSON from the remote repo
   */
  private async fetchJson(path: string): Promise<unknown> {
    const url = `${this.repoBaseUrl}/${path}`;
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status} for ${url}`);
    }
    return response.json();
  }

  /**
   * Fetch text from the remote repo
   */
  private async fetchText(path: string): Promise<string> {
    const url = `${this.repoBaseUrl}/${path}`;
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status} for ${url}`);
    }
    return response.text();
  }
}
