import { HttpProject, HttpProjectKind, IHttpProject, LegacyDataExportToApiProject, Project, PostmanDataTransformer, FileAddOptions } from "@api-client/core/build/browser.js";
import { ArcExportObject, ArcPouchTransformer, ExportArcProjects } from "@api-client/core/build/legacy.js";
import { StoreBindings } from "../../bindings/base/StoreBindings.js";

export interface IAnalyzeResult {
  /**
   * Indicates that there was an error during import analysis.
   */
  error?: boolean;
  /**
   * Error message.
   */
  message?: string;

  /**
   * The source application for the file.
   */
  source?: 'arc' | 'postman' | 'api-client';
  /**
   * The name of the project to import
   */
  name?: string;
  /**
   * Schema version, if any.
   */
  version?: string;
}

const PostmanV20Schema = 'https://schema.getpostman.com/json/collection/v2.0.0/collection.json';
const PostmanV21Schema = 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json';

/**
 * A helper class that controls data import logic.
 */
export class DataImportProcessor {
  importData: unknown | undefined;

  analysis?: IAnalyzeResult;

  #abortCtrl: AbortController;

  signal: AbortSignal;

  constructor() {
    this.#abortCtrl = new AbortController();
    this.signal = this.#abortCtrl.signal;
  }

  abort(): void {
    this.#abortCtrl.abort();
  }

  async fromFile(file: File): Promise<IAnalyzeResult> {
    this.importData = undefined;
    this.analysis = undefined;

    const contents = await file.text();
    let data: unknown | undefined;
    try {
      data = JSON.parse(contents);
    } catch (e) {
      // ...
    }
    return this.analyzeData(data);
  }

  async importProject(db: StoreBindings, space: string, parent?: string): Promise<void> {
    const { importData, analysis } = this;
    if (!importData || !analysis) {
      throw new Error(`You need to initialize import first.`);
    }
    if (analysis.error) {
      throw new Error(`The import data are invalid.`);
    }
    switch (analysis.source) {
      case 'arc': this.importArcProject(importData as ArcExportObject, db, space, parent); break;
      case 'api-client': this.importHttpProject(importData as IHttpProject, db, space, parent); break;
      case 'postman': this.importPostmanProject(importData, analysis, db, space, parent); break;
      default: throw new Error(`Unknown import source: ${analysis.source}.`);
    }
  }

  protected analyzeData(data: unknown): IAnalyzeResult {
    this.importData = data;
    let analysis: IAnalyzeResult;

    if (!data) {
      analysis = {
        error: true,
        message: 'Unable to parse the input file. Not a JSON file.'
      };
    } else if (this.isHttpProject(data)) {
      analysis = this.analyzeHttpProject(data);
    } else if (this.isArcProject(data)) {
      analysis = this.analyzeArcProject(data);
    } else if (this.isPostmanCollection(data)) {
      analysis = this.analyzePostman(data);
    } else {
      analysis = {
        error: true,
        message: 'Unrecognized file. Expected ARC, Postman, or HTTP Client export.'
      };
    }
    this.analysis = analysis;
    return analysis;
  }

  /**
   * Checks whether the passed data contain Postman collection (v2 or v2.1).
   */
  protected isPostmanCollection(data: unknown): boolean {
    const typed = data as { info: { _postman_id: string, name: string, schema: string } }
    if (!typed.info || !typed.info.schema) {
      return false;
    }
    const { schema } = typed.info;
    return [PostmanV20Schema, PostmanV21Schema].includes(schema);
  }

  protected analyzePostman(data: unknown): IAnalyzeResult {
    const typed = data as { info: { _postman_id: string, name: string, schema: string } }
    const { schema, name } = typed.info;
    return {
      name: name || 'Unnamed collection',
      source: 'postman',
      version: schema === PostmanV20Schema ? 'v2' : 'v2.1'
    }
  }

  /**
   * Checks whether the passed data contain ARC's project
   */
  protected isArcProject(data: unknown): boolean {
    const typed = data as ArcExportObject;
    if (typed.kind !== 'ARC#Project') {
      return false;
    }
    return !!typed.projects && typed.projects.length === 1;
  }

  protected analyzeArcProject(data: unknown): IAnalyzeResult {
    const typed = data as ArcExportObject;
    const [project] = typed.projects as ExportArcProjects[];
    return {
      source: 'arc',
      name: project.name,
    }
  }

  /**
   * Checks whether the data is an HttpProject.
   */
  protected isHttpProject(data: unknown): boolean {
    const typed = data as IHttpProject;
    return typed.kind === HttpProjectKind;
  }

  protected analyzeHttpProject(data: unknown): IAnalyzeResult {
    const typed = data as IHttpProject;
    return {
      source: 'api-client',
      name: typed.info.name || 'Unnamed project',
    }
  }

  protected async importArcProject(importData: ArcExportObject, db: StoreBindings, space: string, parent?: string): Promise<void> {
    const transformer = new ArcPouchTransformer(importData);
    const normalized = await transformer.transform();
    const processor = new LegacyDataExportToApiProject();
    const projects = await processor.readProjects(normalized);
    const [data] = projects;
    this.upsert(data, db, space, parent);
  }

  protected async importHttpProject(importData: IHttpProject, db: StoreBindings, space: string, parent?: string): Promise<void> {
    const data = new HttpProject(importData);
    this.upsert(data, db, space, parent);
  }

  protected async importPostmanProject(importData: unknown, analysis: IAnalyzeResult, db: StoreBindings, space: string, parent?: string): Promise<void> {
    const data = await PostmanDataTransformer.transform(importData);
    const project = Array.isArray(data) ? data[0] : data;
    this.upsert(project, db, space, parent);
  }

  protected async upsert(data: HttpProject, db: StoreBindings, space: string, parent?: string): Promise<void> {
    const meta = Project.fromProject(data, space);
    const opts: FileAddOptions = {};
    if (parent) {
      opts.parent = parent;
    }
    // meta file
    const file = await db.files.add(meta.toJSON(), opts);
    // file contents
    await db.media.add(file.key, data.toJSON(), 'application/json');
  }
}
