/**
 * Copyright IBM Corp. 2024, 2025
 */

import { Assert } from '@apic/api-model/test/common/Assert.js';
import { VCM } from '../engine/variable-context-manager/context-manager.js';
import { EnvironmentVariable } from '../models/interface.js';
import { Assertion, AssertionFactory } from './assertion.factory.js';
import { Environment, EnvironmentFactory } from './environment.factory.js';
import { Gateway, GatewayFactory } from './gateway.factory.js';
import { TestFactory } from './test.factory.js';
import { Test } from '../schemas/test.schema.js';

type ExtendedAsset = Assert & { stopOnFail: boolean; if?: string | boolean };

export class ModelFactory {
  private tests = new Map<string, Test>();
  private environments = new Map<string, Environment>();
  private assertions = new Map<string, Assertion>();
  private gateways = new Map<string, Gateway>();
  private testFactory: TestFactory = new TestFactory();

  constructor() {}
  create(raw: any[]): void {
    raw?.forEach((ele: any) => {
      if (!ele?.kind) {
        this.gateways.set('gateway', new GatewayFactory().create(ele));
        return;
      }
      const {
        kind,
        metadata: { name, namespace, version },
      } = ele;
      const key = this.buildModelKey(kind, namespace, name, version);
      switch (kind) {
        case 'test':
          this.tests.set(key, this.testFactory.create(ele));
          break;
        case 'assertion':
          this.assertions.set(key, new AssertionFactory().create(ele));
          break;
        case 'environment':
          this.environments.set(key, new EnvironmentFactory().create(ele));
          break;
        default:
          throw new Error(`Unsupported kind: ${ele.kind}`);
      }
    });
  }

  destroy(): void {
    this.tests.forEach((test) => {
      if (test.vcmId) VCM.deleteContext(test.vcmId);
    });
    this.tests.clear();
    this.environments.clear();
    this.assertions.clear();
  }

  getTest(
    namespace: string,
    name: string,
    version: string,
    endpoint?: string,
  ): Test | undefined {
    return this.tests.get(
      this.buildModelKey('test', namespace, name, version, endpoint),
    );
  }

  getAllTests(): Test[] {
    return Array.from(this.tests.values());
  }

  getAllTestsWithKey(): { key: string; test: Test }[] {
    return Array.from(this.tests.entries()).map(([key, test]) => ({
      key,
      test,
    }));
  }

  getEnvironment(
    namespace: string,
    name: string,
    version: string,
  ): Environment | undefined {
    return this.environments.get(
      this.buildModelKey('environment', namespace, name, version),
    );
  }

  getAllEnvironment(): Environment[] {
    return Array.from(this.environments.values());
  }

  getAssertions(
    namespace: string,
    name: string,
    version: string,
  ): Assertion | undefined {
    return this.assertions.get(
      this.buildModelKey('assertion', namespace, name, version),
    );
  }

  getAllAssertions(): Assertion[] {
    return Array.from(this.assertions.values());
  }

  getGateway(): Gateway | undefined {
    return this.gateways.get('gateway');
  }

  private buildModelKey(
    kind: string,
    namespace: string,
    name: string,
    version: string,
    id?: string,
  ): string {
    return id
      ? `${kind}::${namespace}::${name}::${version}::${id}`
      : `${kind}::${namespace}::${name}::${version}`;
  }

  resolveRefs() {
    // For Test collection logic needs to be updated based on test created by resolve environments
    let tests: { key: string; test: Test }[] = this.getAllTestsWithKey();
    tests.map(({ test }) => {
      // Resolve assertions ref
      this.resolveAssertions(test);
    });

    // For gateway collection logic needs to be updated based on test created by assertions
    tests = this.getAllTestsWithKey();
    tests.map(({ test, key }) => {
      // Resolve api ref
      this.resolveAPI(test, key);
    });

    // For Environment collection logic needs to be updated based on test created by gateways
    tests = this.getAllTestsWithKey();
    tests.map(({ test, key }) => {
      // Resolve environment ref
      this.resolveEnvironment(test, key);
    });
  }

  // Resolve API reference with gateway endpoint if available.
  // If ref is not available, function will be skipped.
  private resolveAPI(test: Test, testKey: string) {
    try {
      if (test.spec?.api?.$ref) {
        let refKeys = test.spec.api.$ref;
        const gateway = this.getGateway();
        if (!Array.isArray(refKeys)) {
          refKeys = [refKeys];
        }
        refKeys.forEach((refKey: string) => {
          const endpoints = gateway?.[refKey];
          if (!endpoints) {
            throw new Error(`Reference variable '${refKey}' not defined`);
          }
          delete test.spec.api.$ref;
          const { kind, metadata } = test;
          const { name, namespace, version } = metadata ?? {};
          endpoints.forEach((endpoint: string) => {
            test.spec!.api!.$endpoint = endpoint;

            // Create a deep copy of the test to avoid modifying the original
            const testCopy = JSON.parse(JSON.stringify(test));

            const parsed = this.testFactory.create(testCopy);
            const key = this.buildModelKey(
              kind!,
              namespace!,
              name!,
              version!,
              `${refKey}:${endpoint}`,
            );
            this.tests.set(key, parsed);
          });
        });
        // Need to delete the test based on environment name too
        this.tests.delete(testKey);
      }
    } catch (error) {
      throw error;
    }
  }
  // Resolve API reference with environment if available.
  // If ref is not available, function will be skipped.
  private resolveEnvironment(test: Test, testKey: string) {
    try {
      // check $ref is array to create test collections
      // for consistency make the string to array
      if (test.spec.environment) {
        let environments: any[] = [];

        if (Array.isArray(test.spec.environment)) {
          // Format 1: array of objects with $ref
          const refArray = test.spec.environment as Array<{ $ref?: string }>;
          environments = refArray.flatMap((refObj) => {
            if (!refObj || !refObj.$ref) return [];

            const ref = refObj.$ref;
            const [namespace, name, version] = ref.split(':');
            const environment = this.getEnvironment(namespace, name, version);
            if (!environment) {
              throw new Error(`${ref} is not available in environment`);
            }
            if (
              !environment.metadata?.name ||
              !environment.metadata?.version ||
              !environment.metadata?.namespace
            ) {
              throw new Error(
                `Environment ${namespace}:${name}:${version} has incomplete metadata`,
              );
            }
            return [environment];
          });
        } else if (test.spec.environment.$ref) {
          const ref = test.spec.environment.$ref;
          const [namespace, name, version] = ref.split(':');
          const environment = this.getEnvironment(namespace, name, version);
          if (!environment) {
            throw new Error(`${ref} is not available in environment`);
          }
          if (
            !environment.metadata?.name ||
            !environment.metadata?.version ||
            !environment.metadata?.namespace
          ) {
            throw new Error(
              `Environment ${namespace}:${name}:${version} has incomplete metadata`,
            );
          }
          environments = [environment];
        } else {
          // actual data is present instead of references to files
          return;
        }

        const { kind: testKind, metadata } = test;
        const {
          name: testName,
          namespace: testNamespace,
          version: testVersion,
        } = metadata ?? {};
        environments.forEach((environment) => {
          // Create a deep copy of the test to avoid modifying the original
          const testCopy = JSON.parse(JSON.stringify(test));
          const envSpecVariables: EnvironmentVariable[] = (environment.spec
            ?.variables ?? []) as EnvironmentVariable[];

          // Build the new environment structure for the test copy
          testCopy.spec.environment = {
            variables: [
              {
                metadata: {
                  name: environment.metadata.name,
                  version: environment.metadata.version,
                  namespace: environment.metadata.namespace,
                },
                kind: 'environment',
                spec: {
                  variables: envSpecVariables,
                },
              },
            ],
          };
          // New test model. Generates new vcmId.
          const parsedTest = this.testFactory.create(testCopy);
          // TestFactory.create() only loads variables from testCopy.spec.environment.
          VCM.loadEnv(parsedTest.vcmId!, envSpecVariables);

          const key = this.buildModelKey(
            testKind!,
            testNamespace!,
            testName!,
            testVersion!,
            `${testKey}${environment.metadata.namespace}:${environment.metadata.name}:${environment.metadata.version}`,
          );
          this.tests.set(key, parsedTest);
        });
        // Based on environment key, new test got created.
        // So test with default settings are removed from registry.
        this.tests.delete(testKey);
      }
    } catch (error) {
      throw error;
    }
  }

  // Resolve API reference with assertions if available.
  // If ref is not available, function will be skipped.
  private resolveAssertions(test: Test) {
    try {
      const requests = test.spec.request;
      requests.forEach((request) => {
        if (request.assertions) {
          let assertions: any[] = [];

          // Handle both formats of assertions
          if (Array.isArray(request.assertions)) {
            // Format 1: array of objects with $ref
            const refArray = request.assertions as Array<{ $ref?: string }>;
            assertions = refArray.flatMap((refObj) => {
              if (!refObj || !refObj.$ref) return [];

              const ref = refObj.$ref;
              const [namespace, name, version] = ref.split(':');
              const assertionModel = this.getAssertions(
                namespace,
                name,
                version,
              );
              if (!assertionModel) {
                throw new Error(`${ref} is not available in assertions`);
              }
              return [assertionModel];
            });
          } else if (request.assertions.$ref) {
            // Format 2: single assertion with direct $ref property
            const ref = request.assertions.$ref;
            const [namespace, name, version] = ref.split(':');
            const assertionModel = this.getAssertions(namespace, name, version);
            if (!assertionModel) {
              throw new Error(`${ref} is not available in assertions`);
            }
            assertions = [assertionModel];
          } else if (request.assertions.assertions) {
            // Format with expressions already defined
            assertions = request.assertions.assertions;
          }
          // Due to APIC model definition we are forced to have below
          request.assertions = {
            //Looping over each items in assertion list
            assertions: assertions.map((data) => {
              //Checking for required fields
              if (!data.metadata || !data.spec) {
                throw new Error('Assertion data is missing required fields');
              }
              //Creating new object from data with metadata, kind and apiVersion
              return {
                metadata: {
                  name: data.metadata.name!,
                  version: data.metadata.version!,
                  namespace: data.metadata.namespace!,
                },
                kind: 'assertion' as const,
                apiVersion: data.apiVersion,
                spec: data.spec.map((a: Assert) => {
                  const extended = a as ExtendedAsset;
                  return {
                    name: a.name ?? '',
                    key: a.key ?? '',
                    action: a.action ?? '',
                    value: a.value,
                    ...(extended.stopOnFail ? { stopOnFail: true } : {}),
                    ...(extended.if !== undefined ? { if: extended.if } : {}),
                  };
                }),
              };
            }),
          };
        }
      });

      // Create a new test instance to validate the schema
      this.testFactory.create(test);
    } catch (error) {
      throw error;
    }
  }
}
