/**
 * Onebox Registry Manager
 *
 * Manages the local Docker registry using:
 * - @push.rocks/smartstorage (S3-compatible server with filesystem storage)
 * - @push.rocks/smartregistry (OCI-compliant Docker registry)
 */

import * as plugins from '../plugins.ts';
import { logger } from '../logging.ts';
import { getErrorMessage } from '../utils/error.ts';

export class RegistryManager {
  private s3Server: any = null;
  private registry: any = null;
  private jwtSecret: string;
  private baseUrl: string;
  private isInitialized = false;

  constructor(private options: {
    dataDir?: string;
    port?: number;
    baseUrl?: string;
  } = {}) {
    this.jwtSecret = this.getJwtSecret();
    this.baseUrl = options.baseUrl || 'localhost:5000';
  }

  /**
   * Initialize the registry (start smartstorage and smartregistry)
   */
  async init(): Promise<void> {
    if (this.isInitialized) {
      logger.warn('Registry already initialized');
      return;
    }

    try {
      const dataDir = this.options.dataDir || './.nogit/registry-data';
      const port = this.options.port || 4000;

      logger.info(`Starting smartstorage server on port ${port}...`);

      // 1. Start smartstorage server (S3-compatible storage with filesystem backend)
      this.s3Server = await plugins.smartstorage.SmartStorage.createAndStart({
        server: {
          port: port,
          address: '0.0.0.0',
        },
        storage: {
          directory: dataDir,
          cleanSlate: false, // Preserve data across restarts
        },
      });

      logger.success(`smartstorage server started on port ${port}`);

      // 2. Configure smartregistry to use smartstorage
      logger.info('Initializing smartregistry...');

      this.registry = new plugins.smartregistry.SmartRegistry({
        storage: {
          endpoint: 'localhost',
          port: port,
          accessKey: 'onebox', // smartstorage doesn't validate credentials
          accessSecret: 'onebox',
          useSsl: false,
          region: 'us-east-1',
          bucketName: 'onebox-registry',
        },
        auth: {
          jwtSecret: this.jwtSecret,
          tokenStore: 'memory',
          npmTokens: {
            enabled: false,
          },
          ociTokens: {
            enabled: true,
            realm: `http://${this.baseUrl}/v2/token`,
            service: 'onebox-registry',
          },
        },
        oci: {
          enabled: true,
          basePath: '',  // Empty basePath - OCI paths are passed directly as /v2/...
        },
      });

      await this.registry.init();

      this.isInitialized = true;
      logger.success('Onebox Registry initialized successfully');
    } catch (error) {
      logger.error(`Failed to initialize registry: ${getErrorMessage(error)}`);
      throw error;
    }
  }

  /**
   * Handle incoming HTTP requests to the registry
   */
  async handleRequest(req: Request): Promise<Response> {
    if (!this.isInitialized) {
      return new Response('Registry not initialized', { status: 503 });
    }

    try {
      // Convert native Request to IRequestContext format expected by smartregistry
      const url = new URL(req.url);
      const headers: Record<string, string> = {};
      req.headers.forEach((value, key) => {
        headers[key] = value;
      });

      const query: Record<string, string> = {};
      url.searchParams.forEach((value, key) => {
        query[key] = value;
      });

      // Read body for non-GET requests
      // smartregistry v2.2.0 handles Uint8Array natively for proper digest calculation
      let body: Uint8Array | undefined;
      if (req.method !== 'GET' && req.method !== 'HEAD') {
        const bodyData = await req.arrayBuffer();
        if (bodyData.byteLength > 0) {
          body = new Uint8Array(bodyData);
        }
      }

      // smartregistry v2.2.0 handles JWT tokens natively and supports Uint8Array body
      const context = {
        method: req.method,
        path: url.pathname,
        headers,
        query,
        body,
        rawBody: body,  // smartregistry uses rawBody for digest calculation
      };

      const result = await this.registry.handleRequest(context);

      // Log the result for debugging
      logger.info(`Registry response: status=${result.status}, headers=${JSON.stringify(result.headers)}`);

      // smartregistry v2.0.0 now properly includes WWW-Authenticate headers on 401 responses

      // Convert IResponse back to native Response
      const responseHeaders = new Headers(result.headers || {});
      let responseBody: BodyInit | null = null;

      if (result.body !== undefined) {
        if (result.body instanceof Uint8Array) {
          responseBody = result.body;
        } else if (typeof result.body === 'string') {
          responseBody = result.body;
        } else {
          responseBody = JSON.stringify(result.body);
          if (!responseHeaders.has('Content-Type')) {
            responseHeaders.set('Content-Type', 'application/json');
          }
        }
      }

      return new Response(responseBody, {
        status: result.status,
        headers: responseHeaders,
      });
    } catch (error) {
      logger.error(`Registry request error: ${getErrorMessage(error)}`);
      return new Response('Internal registry error', { status: 500 });
    }
  }

  /**
   * Get all tags for a repository
   */
  async getImageTags(repository: string): Promise<string[]> {
    if (!this.isInitialized) {
      throw new Error('Registry not initialized');
    }

    try {
      const tags = await this.registry.getTags(repository);
      return tags || [];
    } catch (error) {
      logger.warn(`Failed to get tags for ${repository}: ${getErrorMessage(error)}`);
      return [];
    }
  }

  /**
   * Get the manifest digest for a specific image tag
   */
  async getImageDigest(repository: string, tag: string): Promise<string | null> {
    if (!this.isInitialized) {
      throw new Error('Registry not initialized');
    }

    try {
      // Check if getManifest method exists (API may have changed)
      if (typeof this.registry.getManifest !== 'function') {
        // Method not available in current API version
        return null;
      }

      const manifest = await this.registry.getManifest(repository, tag);
      if (manifest && manifest.digest) {
        return manifest.digest;
      }
      return null;
    } catch (error) {
      // Only log if it's not a "not a function" error
      const errMsg = getErrorMessage(error);
      if (!errMsg.includes('not a function')) {
        logger.warn(`Failed to get digest for ${repository}:${tag}: ${errMsg}`);
      }
      return null;
    }
  }

  /**
   * Delete an image by tag
   */
  async deleteImage(repository: string, tag: string): Promise<void> {
    if (!this.isInitialized) {
      throw new Error('Registry not initialized');
    }

    try {
      await this.registry.deleteManifest(repository, tag);
      logger.info(`Deleted image ${repository}:${tag}`);
    } catch (error) {
      logger.error(`Failed to delete image ${repository}:${tag}: ${getErrorMessage(error)}`);
      throw error;
    }
  }

  /**
   * Get or generate the JWT secret for token signing
   */
  private getJwtSecret(): string {
    // In production, this should be stored securely
    // For now, use a consistent secret stored in environment or generate one
    const secret = Deno.env.get('REGISTRY_JWT_SECRET');
    if (secret) {
      return secret;
    }

    // Generate a random secret (this will be different on each restart)
    // In production, you'd want to persist this
    const randomSecret = crypto.randomUUID() + crypto.randomUUID();
    logger.warn('Using generated JWT secret (will be different on restart)');
    logger.warn('Set REGISTRY_JWT_SECRET environment variable for persistence');
    return randomSecret;
  }

  /**
   * Get the auth manager from the registry
   */
  getAuthManager(): any {
    if (!this.isInitialized) {
      throw new Error('Registry not initialized');
    }
    return this.registry.getAuthManager();
  }

  /**
   * Create an OCI token for Docker authentication
   * @param repository - Repository name (e.g., 'hello-world') or '*' for all
   * @param actions - Actions to allow: 'push', 'pull', or both
   * @param expiresIn - Token expiry in seconds (default: 3600)
   */
  async createOciToken(
    repository: string = '*',
    actions: ('push' | 'pull')[] = ['push', 'pull'],
    expiresIn: number = 3600
  ): Promise<string> {
    if (!this.isInitialized) {
      throw new Error('Registry not initialized');
    }

    const authManager = this.registry.getAuthManager();

    // Create scopes for the token
    const scopes = actions.map(action => `oci:repository:${repository}:${action}`);

    // Create OCI token with scopes
    const token = await authManager.createOciToken('onebox-system', scopes, expiresIn);
    return token;
  }

  /**
   * Get the registry status
   */
  getStatus(): { running: boolean; port: number } {
    return {
      running: this.isInitialized,
      port: this.options.port || 4000,
    };
  }

  /**
   * Get the registry base URL
   */
  getBaseUrl(): string {
    return this.baseUrl;
  }

  /**
   * Get the full image name for a service
   */
  getImageName(serviceName: string, tag: string = 'latest'): string {
    return `${this.baseUrl}/${serviceName}:${tag}`;
  }

  /**
   * Stop the registry and smartstorage server
   */
  async stop(): Promise<void> {
    if (this.registry) {
      try {
        this.registry.destroy?.();
      } catch (error) {
        logger.error(`Error destroying smartregistry: ${getErrorMessage(error)}`);
      }
      this.registry = null;
    }

    if (this.s3Server) {
      try {
        await this.s3Server.stop();
        logger.info('smartstorage server stopped');
      } catch (error) {
        logger.error(`Error stopping smartstorage: ${getErrorMessage(error)}`);
      }
      this.s3Server = null;
    }

    this.isInitialized = false;
    logger.info('Registry stopped');
  }
}
