import { 
  ApiError, Folder, HttpProject, IFile, IFolder, IHttpProject, IProject, ISpace, 
  IUser, PermissionRole, Project, ProjectMock, RouteBuilder, Space, StoreSdk, 
  TrashEntry 
} from "@api-client/core/build/browser.js";
import { WebStoreBindings } from "../../src/bindings/web/WebStoreBindings.js";
import AppInfo from '../../src/pages/api-client/AppInfo.js';

export interface FetchResponse {
  status: number;
  headers: Headers;
  body?: string;
}

export interface HttpOptions extends RequestInit {
  method?: 'GET' | 'POST' | 'PATCH' | 'DELETE';
  headers?: Record<string, string>;
  /**
   * Adds the token to the headers.
   */
  token?: string;
}

export default class HttpHelper {
  user?: IUser;

  users: IUser[] = [];

  token?: string;

  tokens: string[] = [];

  space?: ISpace;

  spaces: ISpace[] = [];

  folder?: IFolder;

  folders: IFolder[] = [];

  project?: IProject;

  projects: IProject[] = [];

  mocking = new ProjectMock();

  baseUri: string;

  store: WebStoreBindings;

  _sdk?: StoreSdk;

  get sdk(): StoreSdk {
    const { _sdk } = this;
    if (!_sdk) {
      throw new Error(`Helper not initialized.`);
    }
    return _sdk;
  }

  constructor(baseUri: string) { 
    this.baseUri = baseUri;
    this.store = new WebStoreBindings(AppInfo, baseUri);
  }

  async initializeSdk(): Promise<void> {
    await this.store.initialize();
    await this.store.global.set({
      key: 'default',
      location: this.baseUri,
      name: 'test env',
      source: 'local-store',
      authenticated: false,
    });
    const authenticated = await this.store.auth.isAuthenticated();
    if (!authenticated) {
      await this.store.auth.authenticate({ updateEnvironment: true });
    }
    this._sdk = this.store.getStore().sdk;
  }

  findUserToken(user: IUser): string {
    const index = this.users.indexOf(user);
    if (index === -1) {
      throw new ApiError(`Unable to find the token for the user`, 400);
    }
    const token = this.tokens[index];
    if (!token) {
      throw new ApiError(`Invalid state. Token is missing.`, 400);
    }
    return token;
  }

  async addUser(opts: { defaultUser?: boolean } = {}): Promise<IUser> {
    const token = await this.createUserToken(this.baseUri);
    const user = await this.sdk.user.me({ token });
    this.tokens.push(token);
    this.users.push(user);
    if (opts.defaultUser) {
      this.user = user;
      this.token = token;
      this.sdk.token = token;
    }
    this.users.push(user);
    return user;
  }

  async addSpace(opts: { defaultSpace?: boolean, user?: IUser } = {}): Promise<ISpace> {
    const { defaultSpace = false, user = this.user } = opts;
    if (!user) {
      throw new ApiError(`User not selected`, 400);
    }
    const token = this.findUserToken(user);
    const space = Space.fromName(this.mocking.lorem.word()).toJSON();
    const record = await this.sdk.spaces.add(space, { token });
    this.spaces.push(record.item!);
    if (defaultSpace) {
      this.space = record.item!;
      this.store.space = record.item!.key;
    }
    return record.item!;
  }

  async shareSpace(targetUser: IUser, role: PermissionRole, opts: { sourceUser?: IUser, space?: ISpace } = {}): Promise<void> {
    const { sourceUser = this.user, space = this.space } = opts;
    if (!sourceUser) {
      throw new ApiError(`Source user not selected`, 400);
    }
    if (!space) {
      throw new ApiError(`Space not selected`, 400);
    }
    const token = this.findUserToken(sourceUser);
    await this.sdk.spaces.addUser(space.key, targetUser.key, role, undefined, { token });
  }

  async shareFile(file: IFile, targetUser: IUser, role: PermissionRole, opts: { sourceUser?: IUser, space?: ISpace } = {}): Promise<void> {
    const { sourceUser = this.user, space = this.space } = opts;
    if (!sourceUser) {
      throw new ApiError(`Source user not selected`, 400);
    }
    if (!space) {
      throw new ApiError(`Space not selected`, 400);
    }
    const token = this.findUserToken(sourceUser);
    await this.sdk.file.addUser(space.key, file.key, targetUser.key, role, undefined, { token });
  }

  async addFolder(opts: { defaultFolder?: boolean, user?: IUser, space?: ISpace, parent?: string } = {}): Promise<IFolder> {
    const { defaultFolder = false, space = this.space, user = this.user, parent } = opts;
    if (!user) {
      throw new ApiError(`User not selected`, 400);
    }
    if (!space) {
      throw new ApiError(`Space not selected`, 400);
    }
    const token = this.findUserToken(user);
    const instance = Folder.fromName(this.mocking.lorem.word(), space.key).toJSON();
    const record = await this.sdk.file.createMeta(instance, space.key, { parent }, { token });
    const folder = record.item as IFolder;
    if (defaultFolder) {
      this.folder = folder;
    }
    this.folders.push(folder);
    return folder;
  }

  async generateFolders(size = 20, opts: { user?: IUser, space?: ISpace, parent?: string } = {}): Promise<IFolder[]> {
    const { space = this.space, user = this.user } = opts;
    if (!user) {
      throw new ApiError(`User not selected`, 400);
    }
    if (!space) {
      throw new ApiError(`Space not selected`, 400);
    }
    const token = this.findUserToken(user);
    const url = this.getUri('/test/generate/folders');
    url.searchParams.set('size', String(size));
    url.searchParams.set('space', space.key);
    const rsp = await this.post(url.toString(), { token });
    const raw = rsp.body as string;
    const data = JSON.parse(raw) as IFolder[];
    if (!Array.isArray(data)) {
      throw new Error(`Unexpected response.`);
    }
    return data;
  }

  async addProject(opts: { defaultProject?: boolean, user?: IUser, space?: ISpace, parent?: string } = {}): Promise<IProject> {
    const { parent, space = this.space, user = this.user, defaultProject = false } = opts;
    if (!user) {
      throw new ApiError(`User not selected`, 400);
    }
    if (!space) {
      throw new ApiError(`Space not selected`, 400);
    }
    const token = this.findUserToken(user);
    const project = HttpProject.fromName(this.mocking.lorem.word()).toJSON();
    const file = Project.fromProject(project, space.key).toJSON();
    const record = await this.sdk.file.create(file, project, space.key, { parent }, { token });
    const createdFile = record.item as IProject;
    if (defaultProject) {
      this.project = createdFile;
    }
    this.projects.push(createdFile);
    return createdFile;
  }

  async generateProjects(size = 20, opts: { user?: IUser, space?: ISpace, parent?: string } = {}): Promise<IHttpProject[]> {
    const { parent, space = this.space, user = this.user } = opts;
    if (!user) {
      throw new ApiError(`User not selected`, 400);
    }
    if (!space) {
      throw new ApiError(`Space not selected`, 400);
    }
    const token = this.findUserToken(user);
    const url = this.getUri('/test/generate/projects');
    url.searchParams.set('size', String(size));
    url.searchParams.set('space', String(space.key));
    url.searchParams.set('owner', user.key);
    if (parent) {
      url.searchParams.set('parent', parent);
    }
    const rsp = await this.post(url.toString(), { token });
    const raw = rsp.body as string;
    const data = JSON.parse(raw) as IHttpProject[];
    if (!Array.isArray(data)) {
      throw new Error(`Unexpected response.`);
    }
    return data;
  }

  async generateTrash(opts: { size?: number, user?: IUser, space?: ISpace } = {}): Promise<TrashEntry[]> {
    const { space = this.space, user = this.user, size } = opts;
    if (!user) {
      throw new ApiError(`User not selected`, 400);
    }
    if (!space) {
      throw new ApiError(`Space not selected`, 400);
    }
    const token = this.findUserToken(user);
    const url = this.getUri('test/generate/trash');
    url.searchParams.set('owner', user.key);
    url.searchParams.set('space', space.key);
    if (size) {
      url.searchParams.set('size', String(size));
    }
    const rsp = await this.post(url.toString(), { token });
    return JSON.parse(rsp.body!);
  }

  getUri(path: string): URL {
    let { baseUri } = this;
    if (!baseUri.endsWith('/')) {
      baseUri += '/';
    }
    return new URL(path, baseUri);
  }

  async get(url: string, opts: HttpOptions = {}): Promise<FetchResponse> {
    const { method = 'GET', headers = {} } = opts;

    const execHeaders = { ...headers };
    if (opts.token) {
      execHeaders.authorization = `Bearer ${opts.token}`;
    }
    const init: RequestInit = {
      method,
      headers: execHeaders,
    };
    const response = await fetch(url, init)
    const result: FetchResponse = {
      status: response.status,
      headers: response.headers,
      body: await response.text(),
    };
    return result;
  }

  async post(url: string, opts: HttpOptions = {}): Promise<FetchResponse> {
    const { method = 'POST', headers = {} } = opts;

    const execHeaders = { ...headers };
    if (opts.token) {
      execHeaders.authorization = `Bearer ${opts.token}`;
    }
    const init: RequestInit = {
      method,
      headers: execHeaders,
    };
    if (opts.body) {
      init.body = opts.body;
    }
    const response = await fetch(url, init)
    const result: FetchResponse = {
      status: response.status,
      headers: response.headers,
      body: await response.text(),
    };
    return result;
  }

  patch(url: string, opts: HttpOptions = {}): Promise<FetchResponse> {
    const options = { ...opts };
    options.method = 'PATCH';
    return this.post(url, options);
  }

  delete(url: string, opts: HttpOptions = {}): Promise<FetchResponse> {
    const options = { ...opts };
    options.method = 'DELETE';
    return this.post(url, options);
  }

  /**
   * Creates unauthenticated session in the backend.
   * @param baseUri Server's base URI (with the prefix).
   * @returns The JWT for unauthenticated user.
   */
  async createSession(baseUri: string): Promise<string> {
    const url = new URL('/sessions', baseUri).toString();
    const result = await this.post(url);
    return result.body as string;
  }

  /**
   * Initializes the authentication session.
   * @param baseUri Server's base URI (with the prefix).
   * @param token The unauthenticated session JWT.
   * @returns The location of the authorization endpoint.
   */
  async getAuthSessionEndpoint(baseUri: string, token: string): Promise<string> {
    const url = new URL('/auth/login', baseUri).toString();
    const result = await this.post(url, {
      token,
    });
    const loc = result.headers.get('location');
    if (!loc) {
      throw new Error(`The location header not returned by the server.`)
    }
    return loc;
  }

  /**
   * Performs session initialization and user authentication.
   * 
   * @param baseUri The base URI of a multi-user server.
   * @returns The JWT that has authenticated user.
   */
  async createUserToken(baseUri: string): Promise<string> {
    const initToken = await this.createSession(baseUri);
    const meRoute = new URL(RouteBuilder.usersMe(), baseUri).toString();
    const preTest = await this.get(meRoute, {
      token: initToken,
    });
    if (preTest.status === 200) {
      return initToken;
    }
    const path = await this.getAuthSessionEndpoint(baseUri, initToken);
    const authUrl = new URL(`/v1${path}`, baseUri);
    // this test server uses mocked OAuth server which always returns user data.
    await this.get(authUrl.toString());
    // when the above finishes we are either authenticated as a user or not.
    // We gonna check the /users/me endpoint for confirmation.
    const result = await this.get(meRoute, {
      token: initToken,
    });
    // we expect a user info
    if (result.status !== 200) {
      throw new Error(`Authentication unsuccessful. Reported status for ${RouteBuilder.usersMe()}: ${result.status}`);
    }
    return initToken;
  }

  async clearFiles(): Promise<void> {
    await this.delete(`${this.baseUri}/test/reset/files`);
  }

  async clearSpaces(): Promise<void> {
    await this.delete(`${this.baseUri}/test/reset/spaces`);
  }

  async clearPermissions(): Promise<void> {
    await this.delete(`${this.baseUri}/test/reset/permissions`);
  }

  async clearSessions(): Promise<void> {
    await this.delete(`${this.baseUri}/test/reset/sessions`);
  }
}
