import fs from "fs";
import {
  OpenAPISpec,
  OpenAPIOperation,
  TerraformResource,
  TerraformDataSource,
  TerraformAttribute,
} from "./Types";
import { StringUtils } from "./StringUtils";

export class OpenAPIParser {
  public spec: OpenAPISpec | null = null;

  public async parseOpenAPISpec(filePath: string): Promise<OpenAPISpec> {
    try {
      const content: string = fs.readFileSync(filePath, "utf-8");
      this.spec = JSON.parse(content) as OpenAPISpec;
      return this.spec;
    } catch (error) {
      throw new Error(
        `Failed to parse OpenAPI spec: ${error instanceof Error ? error.message : "Unknown error"}`,
      );
    }
  }

  public setSpec(spec: OpenAPISpec): void {
    this.spec = spec;
  }

  public getResources(): TerraformResource[] {
    if (!this.spec) {
      throw new Error("OpenAPI spec not loaded. Call parseOpenAPISpec first.");
    }

    const resources: TerraformResource[] = [];
    const resourceMap: Map<string, Partial<TerraformResource>> = new Map<
      string,
      Partial<TerraformResource>
    >();

    // Group operations by resource
    for (const [path, pathItem] of Object.entries(this.spec.paths)) {
      for (const [method, operation] of Object.entries(pathItem)) {
        if (
          !operation.operationId ||
          !operation.tags ||
          operation.tags.length === 0
        ) {
          continue;
        }

        const resourceName: string | null = this.extractResourceName(
          path,
          operation,
        );
        if (!resourceName) {
          continue;
        }

        if (!resourceMap.has(resourceName)) {
          resourceMap.set(resourceName, {
            name: resourceName,
            goTypeName: StringUtils.toPascalCase(resourceName),
            operations: {},
            schema: {},
          });
        }

        const resource: Partial<TerraformResource> =
          resourceMap.get(resourceName)!;
        const operationType:
          | "create"
          | "read"
          | "update"
          | "delete"
          | "list"
          | null = this.getOperationType(method, path, operation);

        if (operationType && resource.operations) {
          // Add method and path to operation for later use
          const enhancedOperation: OpenAPIOperation & {
            method: string;
            path: string;
          } = {
            ...operation,
            method: method,
            path: path,
          };
          resource.operations[operationType] = enhancedOperation;
        }
      }
    }

    // Convert to array and generate schemas
    for (const resource of resourceMap.values()) {
      if (resource.name && resource.goTypeName && resource.operations) {
        resource.schema = this.generateResourceSchema(
          resource.operations,
          resource.name,
        );
        resource.operationSchemas = this.generateOperationSpecificSchemas(
          resource.operations,
          resource.name,
        );
        resources.push(resource as TerraformResource);
      }
    }

    return resources;
  }

  public getDataSources(): TerraformDataSource[] {
    if (!this.spec) {
      throw new Error("OpenAPI spec not loaded. Call parseOpenAPISpec first.");
    }

    const dataSources: TerraformDataSource[] = [];
    const dataSourceMap: Map<string, Partial<TerraformDataSource>> = new Map<
      string,
      Partial<TerraformDataSource>
    >();

    // Look for GET and POST operations that can be used as data sources
    for (const [path, pathItem] of Object.entries(this.spec.paths)) {
      for (const [method, operation] of Object.entries(pathItem)) {
        if (
          !operation.operationId ||
          !operation.tags ||
          operation.tags.length === 0
        ) {
          continue;
        }

        // Check if this is a read operation (GET or POST with read-like operation)
        const isReadOperation: boolean = this.isReadOperation(
          method,
          path,
          operation,
        );
        if (!isReadOperation) {
          continue;
        }

        const resourceName: string | null = this.extractResourceName(
          path,
          operation,
        );
        if (!resourceName) {
          continue;
        }

        const dataSourceName: string = `${resourceName}_data`;

        if (!dataSourceMap.has(dataSourceName)) {
          dataSourceMap.set(dataSourceName, {
            name: dataSourceName,
            goTypeName: StringUtils.toPascalCase(dataSourceName),
            operations: {},
            schema: {},
          });
        }

        const dataSource: Partial<TerraformDataSource> =
          dataSourceMap.get(dataSourceName)!;
        const isListOperation: boolean = this.isListOperation(path, operation);

        if (dataSource.operations) {
          // Add method and path to operation for later use
          const enhancedOperation: OpenAPIOperation & {
            method: string;
            path: string;
          } = {
            ...operation,
            method: method,
            path: path,
          };

          if (isListOperation) {
            dataSource.operations.list = enhancedOperation;
          } else {
            dataSource.operations.read = enhancedOperation;
          }
        }
      }
    }

    // Convert to array and generate schemas
    for (const dataSource of dataSourceMap.values()) {
      if (dataSource.name && dataSource.goTypeName && dataSource.operations) {
        dataSource.schema = this.generateDataSourceSchema(
          dataSource.operations,
        );
        dataSources.push(dataSource as TerraformDataSource);
      }
    }

    return dataSources;
  }

  private extractResourceName(
    path: string,
    operation: OpenAPIOperation,
  ): string | null {
    // Try to extract from tags first
    if (operation.tags && operation.tags.length > 0 && operation.tags[0]) {
      return StringUtils.toSnakeCase(operation.tags[0]);
    }

    // Fallback to path analysis
    const pathSegments: string[] = path.split("/").filter((segment: string) => {
      return segment && !segment.startsWith("{");
    });
    if (pathSegments.length > 0) {
      const lastSegment: string | undefined =
        pathSegments[pathSegments.length - 1];
      if (lastSegment) {
        return StringUtils.toSnakeCase(lastSegment);
      }
    }

    return null;
  }

  private getOperationType(
    method: string,
    path: string,
    operation: OpenAPIOperation,
  ): "create" | "read" | "update" | "delete" | "list" | null {
    const lowerMethod: string = method.toLowerCase();
    const hasIdParam: boolean = path.includes("{id}");

    switch (lowerMethod) {
      case "post":
        // Check if this is a read operation based on operation ID or path
        if (this.isReadOperation(method, path, operation)) {
          return this.isListOperation(path, operation) ? "list" : "read";
        }

        if (hasIdParam) {
          // POST to /{resource}/{id} is usually a read operation in OneUptime API
          return "read";
        }
        // POST to /{resource} is create
        return "create";

      case "get":
        return this.isListOperation(path, operation) ? "list" : "read";
      case "put":
      case "patch":
        return "update";
      case "delete":
        return "delete";
      default:
        return null;
    }
  }

  private isListOperation(path: string, operation: OpenAPIOperation): boolean {
    // Check if path ends with collection (not individual resource)
    const hasIdParam: boolean =
      path.includes("{id}") || (path.includes("{") && path.endsWith("}"));

    // Check for explicit list patterns in the path
    const pathSegments: string[] = path.toLowerCase().split("/");
    const hasListPathPattern: boolean = pathSegments.some((segment: string) => {
      return (
        segment.includes("get-list") ||
        segment.includes("list") ||
        segment === "count"
      );
    });

    // Check operation ID for list patterns
    const operationId: string = operation.operationId?.toLowerCase() || "";
    const hasListOperationId: boolean = operationId.includes("list");

    return !hasIdParam || hasListPathPattern || hasListOperationId;
  }

  private generateResourceSchema(
    operations: any,
    resourceName?: string,
  ): Record<string, TerraformAttribute> {
    const schema: Record<string, TerraformAttribute> = {};

    // Always add id field for resources
    schema["id"] = {
      type: "string",
      description: "Unique identifier for the resource",
      computed: true,
    };

    // First pass: Extract schema from create/update operations (input fields)
    // These define the required fields for the resource
    if (operations.create) {
      this.addSchemaFromOperation(
        schema,
        operations.create,
        false,
        `${resourceName}-create`,
      );
    }
    if (operations.update) {
      this.addSchemaFromOperation(
        schema,
        operations.update,
        false,
        `${resourceName}-update`,
      );
    }

    // Store which fields are in create/update operations before adding read fields
    const createUpdateFields: Set<string> = new Set<string>();
    const requiredFields: Set<string> = new Set<string>();
    for (const [fieldName, attr] of Object.entries(schema)) {
      createUpdateFields.add(fieldName);
      if ((attr as any).required) {
        requiredFields.add(fieldName);
      }
    }

    // Second pass: Extract schema from read operations (ensure all output fields are included)
    if (operations.read) {
      this.addSchemaFromOperation(
        schema,
        operations.read,
        true,
        `${resourceName}-read`,
      );
    }

    // Third pass: Identify fields that should be both optional and computed
    // These are fields that:
    // 1. Appear in both create/update AND read operations, but
    // 2. Are not required in create/update operations
    // This indicates server-managed fields that can be optionally set by users
    for (const [fieldName, attr] of Object.entries(schema)) {
      const isInCreateUpdate: boolean = createUpdateFields.has(fieldName);
      const isRequired: boolean = requiredFields.has(fieldName);
      const isComputed: boolean = Boolean(attr.computed);

      if (isInCreateUpdate && !isRequired && isComputed) {
        // Field is optional in create/update but computed in read
        // This means it should be both optional and computed
        schema[fieldName] = {
          ...attr,
          required: false,
          computed: true,
          optional: true, // Mark as explicitly optional and computed
        };
      } else if (isRequired) {
        // Restore required status for fields that were required in create operations
        schema[fieldName] = {
          ...attr,
          required: true,
          computed: false,
        };
      }
    }

    return schema;
  }

  private generateOperationSpecificSchemas(
    operations: any,
    resourceName: string,
  ): {
    create?: Record<string, TerraformAttribute>;
    update?: Record<string, TerraformAttribute>;
    read?: Record<string, TerraformAttribute>;
  } {
    const operationSchemas: any = {};

    // Generate schema for create operation
    if (operations.create) {
      const createSchema: Record<string, TerraformAttribute> = {};
      this.addSchemaFromOperation(
        createSchema,
        operations.create,
        false,
        `${resourceName}-create-only`,
      );
      operationSchemas.create = createSchema;
    }

    // Generate schema for update operation
    if (operations.update) {
      const updateSchema: Record<string, TerraformAttribute> = {};
      this.addSchemaFromOperation(
        updateSchema,
        operations.update,
        false,
        `${resourceName}-update-only`,
      );
      operationSchemas.update = updateSchema;
    }

    // Generate schema for read operation
    if (operations.read) {
      const readSchema: Record<string, TerraformAttribute> = {};
      this.addSchemaFromOperation(
        readSchema,
        operations.read,
        true,
        `${resourceName}-read-only`,
      );
      operationSchemas.read = readSchema;
    }

    return operationSchemas;
  }

  private generateDataSourceSchema(
    operations: any,
  ): Record<string, TerraformAttribute> {
    const schema: Record<string, TerraformAttribute> = {};

    // Add filter fields for data sources
    schema["id"] = {
      type: "string",
      description: "Identifier to filter by",
      required: false,
    };

    schema["name"] = {
      type: "string",
      description: "Name to filter by",
      required: false,
    };

    // Extract schema from read operations
    if (operations.read) {
      this.addSchemaFromOperation(
        schema,
        operations.read,
        true,
        "datasource-read",
      );
    }
    if (operations.list) {
      this.addSchemaFromOperation(
        schema,
        operations.list,
        true,
        "datasource-list",
      );
    }

    return schema;
  }

  private addSchemaFromOperation(
    schema: Record<string, TerraformAttribute>,
    operation: OpenAPIOperation,
    computed: boolean,
    context?: string,
  ): void {
    // Add parameters as schema fields
    if (operation.parameters) {
      for (const param of operation.parameters) {
        if (param.in === "path" || param.name === "id") {
          continue;
        }

        schema[StringUtils.toSnakeCase(param.name)] = {
          type: this.mapOpenAPITypeToTerraform(param.schema?.type || "string"),
          description: param.description || "",
          required: computed ? false : param.required || false,
          computed: computed,
          apiFieldName: param.name, // Preserve original OpenAPI parameter name
        };
      }
    }

    // Add request body schema fields - look inside the data wrapper
    if (operation.requestBody && !computed) {
      const content: any = operation.requestBody.content?.["application/json"];
      if (content?.schema?.properties?.["data"]) {
        const dataSchema: any = content.schema.properties["data"];
        this.addSchemaFromOpenAPISchema(
          schema,
          dataSchema,
          computed,
          `${context}-requestBody`,
        );
      }
    }

    // Add response schema fields for read operations - look inside the data wrapper
    if (computed && operation.responses) {
      const successResponse: any =
        operation.responses["200"] || operation.responses["201"];
      if (
        successResponse?.content?.["application/json"]?.schema?.properties?.[
          "data"
        ]
      ) {
        const dataSchema: any =
          successResponse.content["application/json"].schema.properties["data"];
        this.addSchemaFromOpenAPISchema(
          schema,
          dataSchema,
          true,
          `${context}-response`,
        );
      }
    }
  }

  private addSchemaFromOpenAPISchema(
    schema: Record<string, TerraformAttribute>,
    openApiSchema: any,
    computed: boolean,
    context?: string,
  ): void {
    // Handle $ref schemas
    if (openApiSchema.$ref) {
      const resolvedSchema: any = this.resolveSchemaRef(openApiSchema.$ref);
      if (resolvedSchema) {
        this.addSchemaFromOpenAPISchema(
          schema,
          resolvedSchema,
          computed,
          `${context}-ref`,
        );
      }
      return;
    }

    // Check if this schema has properties defined
    const hasProperties: boolean =
      openApiSchema.properties &&
      Object.keys(openApiSchema.properties).length > 0;

    // If no properties are defined (empty CRUD schema), try to fallback to main model schema
    if (!hasProperties && openApiSchema.description) {
      const description: string = openApiSchema.description.toLowerCase();
      if (description.includes("schema for") && description.includes("model")) {
        // Extract model name from description like "Create schema for Label model. Create"
        const modelNameMatch: RegExpMatchArray | null = description.match(
          /schema for (\w+) model/,
        );
        if (modelNameMatch && modelNameMatch[1]) {
          const modelName: string = modelNameMatch[1]; // Keep original case from the description
          const mainModelSchema: any = this.resolveSchemaRef(
            `#/components/schemas/${modelName}`,
          );
          if (mainModelSchema && mainModelSchema.properties) {
            this.addSchemaFromOpenAPISchema(
              schema,
              mainModelSchema,
              computed,
              `${context}-fallback-${modelName}`,
            );
            return;
          }
        }
      }
    }

    if (openApiSchema.properties) {
      for (const [propName, propSchema] of Object.entries(
        openApiSchema.properties,
      )) {
        const terraformName: string = StringUtils.toSnakeCase(propName);
        if (terraformName === "id" || terraformName === "_id") {
          // Skip ID fields as they're handled separately
          continue;
        }

        const prop: any = propSchema as any;
        let propType: string = prop.type || "string";
        let propFormat: string | undefined = prop.format; // Capture the format field
        let description: string = prop.description || "";
        let example: any = prop.example;
        let defaultValue: any = prop.default;

        // Handle nested $ref
        if (prop.$ref) {
          const resolvedProp: any = this.resolveSchemaRef(prop.$ref);
          if (resolvedProp) {
            propType = resolvedProp.type || "string";
            propFormat = resolvedProp.format || propFormat; // Also capture format from resolved refs
            description = resolvedProp.description || description;
            example = resolvedProp.example || example;
            defaultValue = resolvedProp.default || defaultValue;
          }
        }

        // Skip computed fields for create/update operations
        const isComputedField: boolean = [
          "createdAt",
          "updatedAt",
          "deletedAt",
          "version",
          "slug",
          "createdByUserId",
          "deletedByUserId",
        ].includes(propName);
        if (!computed && isComputedField) {
          continue;
        }

        // If field already exists and we're adding computed fields, check if it should be both optional and computed
        if (computed && schema[terraformName]) {
          // Update description if it's better in the read schema
          const existingField: any = schema[terraformName];
          if (existingField && description && !existingField.description) {
            existingField.description = description;
          }

          // If the field exists from create/update and now appears in read,
          // it should be marked as both optional and computed (server-managed field)
          if (existingField && !existingField.required) {
            schema[terraformName] = {
              ...existingField,
              computed: true,
              optional: true, // Mark as explicitly optional and computed
            };
          }
          continue;
        }

        // If field already exists and we're adding input fields, merge the properties
        if (
          !computed &&
          schema[terraformName] &&
          schema[terraformName]?.computed
        ) {
          // Update the existing computed field to also be an input field
          const existingField: any = schema[terraformName];
          if (existingField) {
            schema[terraformName] = {
              ...existingField,
              required: openApiSchema.required?.includes(propName) || false,
              computed: false, // This field can be both input and output
              apiFieldName: propName, // Preserve original OpenAPI property name
            };
          }
          continue;
        }

        // Determine if this field should be required
        let fieldRequired: boolean = false;
        if (computed) {
          fieldRequired = false; // Computed fields are never required
        } else {
          // Check if it's explicitly required in the current schema
          const explicitlyRequired: boolean =
            openApiSchema.required?.includes(propName) || false;

          // If the field already exists and was previously marked as required, preserve that
          const existingField: TerraformAttribute | undefined =
            schema[terraformName];
          const previouslyRequired: boolean = existingField?.required || false;

          // Field is required if it's explicitly required OR was previously required
          fieldRequired = explicitlyRequired || previouslyRequired;
        }

        schema[terraformName] = {
          type: this.mapOpenAPITypeToTerraform(propType),
          description: description,
          required: fieldRequired,
          computed: computed || isComputedField,
          apiFieldName: propName, // Preserve original OpenAPI property name
          example: example, // Extract example from OpenAPI schema
          default: defaultValue, // Extract default value from OpenAPI schema
          isComplexObject: propType === "object", // Flag to indicate this string field is actually a complex object
          ...(propFormat && { format: propFormat }), // Only include format if it exists
        };
      }
    }
  }

  private resolveSchemaRef(ref: string): any {
    if (!this.spec || !ref.startsWith("#/components/schemas/")) {
      return null;
    }

    const schemaName: string = ref.replace("#/components/schemas/", "");
    return this.spec.components?.schemas?.[schemaName] || null;
  }

  private mapOpenAPITypeToTerraform(openApiType: string): string {
    switch (openApiType) {
      case "integer":
      case "number":
        return "number";
      case "boolean":
        return "bool";
      case "array":
        return "list";
      case "object":
        // For now, treat complex objects as JSON strings to handle nested structures
        // This allows users to pass complex nested objects that will be serialized to JSON
        return "string";
      default:
        return "string";
    }
  }

  private isReadOperation(
    method: string,
    path: string,
    operation: OpenAPIOperation,
  ): boolean {
    const lowerMethod: string = method.toLowerCase();

    // Traditional GET operations are always read operations
    if (lowerMethod === "get") {
      return true;
    }

    // Check for POST operations that are actually read operations
    if (lowerMethod === "post") {
      const operationId: string = operation.operationId?.toLowerCase() || "";

      // Check operation ID patterns for read operations
      const readPatterns: string[] = [
        "get",
        "list",
        "find",
        "search",
        "retrieve",
        "fetch",
      ];

      const isReadOperationId: boolean = readPatterns.some(
        (pattern: string) => {
          return operationId.includes(pattern);
        },
      );

      // Check path patterns for read operations
      const pathSegments: string[] = path.toLowerCase().split("/");
      const hasReadPathPattern: boolean = pathSegments.some(
        (segment: string) => {
          return (
            segment.includes("get-") ||
            segment.includes("list") ||
            segment.includes("search") ||
            segment.includes("find")
          );
        },
      );

      return isReadOperationId || hasReadPathPattern;
    }

    return false;
  }
}
