/**
 * 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.js';
import { logger } from '../logging.js';
import { getErrorMessage } from '../utils/error.js';

type TOciTags = Record<string, string>;

interface IOciDescriptor {
  digest?: unknown;
}

interface IOciManifestDocument {
  config?: IOciDescriptor;
  layers?: IOciDescriptor[];
  manifests?: IOciDescriptor[];
}

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.readOciTags(repository);
      return Object.keys(tags).sort();
    } 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 {
      const digest = this.normalizeOciDigest(tag);
      if (digest) return digest;

      const tags = await this.readOciTags(repository);
      return tags[tag] || null;
    } catch (error) {
      logger.warn(`Failed to get digest for ${repository}:${tag}: ${getErrorMessage(error)}`);
      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 {
      const storage = this.getRegistryStorage();
      const tags = await this.readOciTags(repository, storage);
      const targetDigest = this.normalizeOciDigest(tag) || tags[tag];

      if (!targetDigest) {
        logger.info(`Registry image ${repository}:${tag} does not exist`);
        return;
      }

      const remainingTags: TOciTags = { ...tags };
      if (this.normalizeOciDigest(tag)) {
        for (const [tagName, tagDigest] of Object.entries(remainingTags)) {
          if (tagDigest === targetDigest) {
            delete remainingTags[tagName];
          }
        }
      } else {
        delete remainingTags[tag];
      }

      const protectedRoots = Array.from(new Set(Object.values(remainingTags)));
      const targetObjects = await this.collectReferencedOciObjects(storage, repository, [
        targetDigest,
      ]);
      const protectedObjects = await this.collectReferencedOciObjects(
        storage,
        repository,
        protectedRoots,
      );
      this.mergeOciObjects(
        protectedObjects,
        await this.collectTaggedOciObjectsOutsideRepository(storage, repository),
      );

      for (const blobDigest of targetObjects.blobDigests) {
        if (!protectedObjects.blobDigests.has(blobDigest)) {
          await this.deleteOciBlobByDigest(storage, blobDigest);
        }
      }

      for (const manifestDigest of targetObjects.manifestDigests) {
        if (!protectedObjects.manifestDigests.has(manifestDigest)) {
          await this.deleteOciManifestByDigest(storage, repository, manifestDigest);
        }
      }

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

  private getRegistryStorage(): any {
    if (!this.isInitialized) {
      throw new Error('Registry not initialized');
    }
    if (typeof this.registry?.getStorage !== 'function') {
      throw new Error('Registry storage API is not available');
    }
    return this.registry.getStorage();
  }

  private getOciTagsPath(repository: string): string {
    return `oci/tags/${repository}/tags.json`;
  }

  private normalizeOciDigest(digest: string | null | undefined): string | null {
    if (typeof digest !== 'string') return null;
    const trimmedDigest = digest.trim().toLowerCase();
    return /^sha256:[a-f0-9]{64}$/.test(trimmedDigest) ? trimmedDigest : null;
  }

  private getSha256HashFromDigest(digest: string): string {
    const normalizedDigest = this.normalizeOciDigest(digest);
    if (!normalizedDigest) {
      throw new Error(`Invalid OCI digest: ${digest}`);
    }
    return normalizedDigest.slice('sha256:'.length);
  }

  private getOciManifestObjectPath(repository: string, digest: string): string {
    return `oci/manifests/${repository}/${this.getSha256HashFromDigest(digest)}`;
  }

  private getOciBlobObjectPath(digest: string): string {
    return `oci/blobs/sha256/${this.getSha256HashFromDigest(digest)}`;
  }

  private decodeStorageObject(data: unknown): string {
    if (data instanceof Uint8Array) {
      return new TextDecoder().decode(data);
    }
    if (typeof data === 'string') {
      return data;
    }
    const stringLike = data as { toString?: (encoding?: string) => string };
    if (typeof stringLike?.toString === 'function') {
      return stringLike.toString('utf-8');
    }
    return '';
  }

  private async readOciTags(
    repository: string,
    storage = this.getRegistryStorage(),
  ): Promise<TOciTags> {
    const data = await storage.getObject(this.getOciTagsPath(repository));
    if (!data) return {};

    const parsedTags = JSON.parse(this.decodeStorageObject(data));
    if (!parsedTags || typeof parsedTags !== 'object' || Array.isArray(parsedTags)) {
      throw new Error(`Invalid OCI tags document for ${repository}`);
    }

    const tags: TOciTags = {};
    for (const [tagName, digestValue] of Object.entries(parsedTags)) {
      const digest = typeof digestValue === 'string' ? this.normalizeOciDigest(digestValue) : null;
      if (!digest) {
        throw new Error(`Invalid OCI digest for ${repository}:${tagName}`);
      }
      tags[tagName] = digest;
    }
    return tags;
  }

  private async writeOciTags(
    repository: string,
    tags: TOciTags,
    storage = this.getRegistryStorage(),
  ): Promise<void> {
    const tagsPath = this.getOciTagsPath(repository);
    if (Object.keys(tags).length === 0) {
      await this.deleteObjectIfExists(storage, tagsPath);
      return;
    }
    await storage.putObject(tagsPath, new TextEncoder().encode(JSON.stringify(tags, null, 2)));
  }

  private async readOciManifest(
    storage: any,
    repository: string,
    digest: string,
  ): Promise<IOciManifestDocument | null> {
    const data = await storage.getOciManifest(repository, digest);
    if (!data) return null;

    try {
      const manifest = JSON.parse(this.decodeStorageObject(data));
      return manifest && typeof manifest === 'object' ? manifest : null;
    } catch (error) {
      logger.warn(
        `Failed to parse OCI manifest ${repository}@${digest}: ${getErrorMessage(error)}`,
      );
      return null;
    }
  }

  private getDescriptorDigest(descriptor: IOciDescriptor | undefined): string | null {
    return typeof descriptor?.digest === 'string'
      ? this.normalizeOciDigest(descriptor.digest)
      : null;
  }

  private collectOciManifestReferences(manifest: IOciManifestDocument): {
    blobDigests: string[];
    manifestDigests: string[];
  } {
    const blobDigests = [
      this.getDescriptorDigest(manifest.config),
      ...(manifest.layers || []).map((descriptor) => this.getDescriptorDigest(descriptor)),
    ].filter((digest): digest is string => Boolean(digest));

    const manifestDigests = (manifest.manifests || [])
      .map((descriptor) => this.getDescriptorDigest(descriptor))
      .filter((digest): digest is string => Boolean(digest));

    return { blobDigests, manifestDigests };
  }

  private async collectReferencedOciObjects(
    storage: any,
    repository: string,
    rootDigests: string[],
  ): Promise<{ manifestDigests: Set<string>; blobDigests: Set<string> }> {
    const manifestDigests = new Set<string>();
    const blobDigests = new Set<string>();
    const pendingManifestDigests = rootDigests
      .map((digest) => this.normalizeOciDigest(digest))
      .filter((digest): digest is string => Boolean(digest));

    while (pendingManifestDigests.length > 0) {
      const manifestDigest = pendingManifestDigests.shift()!;
      if (manifestDigests.has(manifestDigest)) continue;
      manifestDigests.add(manifestDigest);

      const manifest = await this.readOciManifest(storage, repository, manifestDigest);
      if (!manifest) continue;

      const references = this.collectOciManifestReferences(manifest);
      for (const blobDigest of references.blobDigests) {
        blobDigests.add(blobDigest);
      }
      for (const childManifestDigest of references.manifestDigests) {
        if (!manifestDigests.has(childManifestDigest)) {
          pendingManifestDigests.push(childManifestDigest);
        }
      }
    }

    return { manifestDigests, blobDigests };
  }

  private async collectTaggedOciObjectsOutsideRepository(
    storage: any,
    repository: string,
  ): Promise<{ manifestDigests: Set<string>; blobDigests: Set<string> }> {
    const protectedObjects = {
      manifestDigests: new Set<string>(),
      blobDigests: new Set<string>(),
    };

    const tagPaths = await storage.listObjects('oci/tags/');
    for (const tagPath of tagPaths) {
      const match = tagPath.match(/^oci\/tags\/(.+)\/tags\.json$/);
      if (!match || match[1] === repository) continue;

      const tags = await this.readOciTags(match[1], storage);
      const references = await this.collectReferencedOciObjects(
        storage,
        match[1],
        Object.values(tags),
      );
      for (const manifestDigest of references.manifestDigests) {
        protectedObjects.manifestDigests.add(manifestDigest);
      }
      for (const blobDigest of references.blobDigests) {
        protectedObjects.blobDigests.add(blobDigest);
      }
    }

    return protectedObjects;
  }

  private mergeOciObjects(
    targetArg: { manifestDigests: Set<string>; blobDigests: Set<string> },
    sourceArg: { manifestDigests: Set<string>; blobDigests: Set<string> },
  ): void {
    for (const manifestDigest of sourceArg.manifestDigests) {
      targetArg.manifestDigests.add(manifestDigest);
    }
    for (const blobDigest of sourceArg.blobDigests) {
      targetArg.blobDigests.add(blobDigest);
    }
  }

  private async deleteObjectIfExists(storage: any, path: string): Promise<void> {
    if (typeof storage.objectExists === 'function' && !(await storage.objectExists(path))) {
      return;
    }
    await storage.deleteObject(path);
  }

  private async deleteOciManifestByDigest(
    storage: any,
    repository: string,
    digest: string,
  ): Promise<void> {
    const manifestPath = this.getOciManifestObjectPath(repository, digest);
    await this.deleteObjectIfExists(storage, manifestPath);
    await this.deleteObjectIfExists(storage, `${manifestPath}.type`);
  }

  private async deleteOciBlobByDigest(storage: any, digest: string): Promise<void> {
    await this.deleteObjectIfExists(storage, this.getOciBlobObjectPath(digest));
  }

  /**
   * 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 = process.env.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');
  }
}
