import { AuthListAPIType, AuthListType } from "./types/auth";
import { BaseAPIResponse } from "./types/api";
import { CapabilitiesType } from "./types/capabilities";
import {
  DecryptionPayload,
  DecryptionResult,
  EncryptionPayload,
  EncryptionResult,
  RewrapPayload,
  RewrapResult,
  TransitKeyType,
} from "./types/transit";
import { DoesNotExistError } from "../types/internalErrors";
import { MountType, MountsType, NewMountParams } from "./types/mount";
import { NewTOTPData, NewTOTPResp } from "./types/totp";
import { SealStatusType } from "./types/seal";
import { SecretMetadataType } from "./types/secret";
import { Settings } from "../settings/Settings";
import { TokenInfo } from "./types/token";
import { UserType, UserTypeAPIResp } from "./types/user";
import { getObjectKeys, removeDoubleSlash } from "../utils";

async function checkResponse(resp: Response): Promise<void> {
  if (resp.ok) return;
  if (resp.status == 404) throw DoesNotExistError;

  let json: BaseAPIResponse;
  try {
    json = (await resp.json()) as BaseAPIResponse;
  } catch {
    // Do Nothing
  }

  if (json?.errors?.length >= 1) {
    throw new Error(json.errors[0]);
  }
}

export class API {
  private settings: Settings;

  constructor(settings: Settings) {
    this.settings = settings;
  }

  getHeaders(): Record<string, string> {
    return {
      "X-Vault-Token": this.settings.token,
    };
  }
  appendAPIURL(url: string): string {
    return this.settings.apiURL + url;
  }

  // List all supported auth methods
  async listAuth(): Promise<AuthListType> {
    const request = new Request(this.appendAPIURL(`/v1/sys/auth`), {
      headers: this.getHeaders(),
    });
    const resp = await fetch(request);
    await checkResponse(resp);

    const data = (await resp.json()) as AuthListAPIType;
    return data.data;
  }

  // Tries to login with username and password, returns token
  async usernameLogin(username: string, password: string): Promise<string> {
    const request = new Request(this.appendAPIURL(`/v1/auth/userpass/login/${username}`), {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ username: username, password: password }),
    });

    const resp = await fetch(request);
    await checkResponse(resp);

    const data = (await resp.json()) as {
      auth: { client_token: string };
    };
    return data.auth.client_token;
  }

  async createOrUpdateUserPassUser(
    path: string,
    username: string,
    data: Partial<UserType>,
  ): Promise<void> {
    const request = new Request(
      this.appendAPIURL(removeDoubleSlash(`/v1/auth/${path}/users/${username}`)),
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          ...this.getHeaders(),
        },
        body: JSON.stringify(data, null, 0),
      },
    );
    const resp = await fetch(request);
    await checkResponse(resp);
  }

  async deleteUserPassUser(path: string, username: string): Promise<void> {
    const request = new Request(
      this.appendAPIURL(removeDoubleSlash(`/v1/auth/${path}/users/${username}`)),
      {
        method: "DELETE",
        headers: this.getHeaders(),
      },
    );
    const resp = await fetch(request);
    await checkResponse(resp);
  }

  async getUserPassUser(path: string, username: string): Promise<UserType> {
    const request = new Request(this.appendAPIURL(`/v1/auth/${path}/users/${username}`), {
      headers: this.getHeaders(),
    });
    const resp = await fetch(request);
    await checkResponse(resp);

    const data = (await resp.json()) as UserTypeAPIResp;
    return data.data;
  }

  async listUserPassUsers(path: string): Promise<string[]> {
    const request = new Request(this.appendAPIURL(`/v1/auth/${path}/users?list=true`), {
      headers: this.getHeaders(),
    });
    const resp = await fetch(request);
    await checkResponse(resp);

    const data = (await resp.json()) as { data: { keys: string[] } };
    return data.data.keys;
  }

  async getMount(mountName: string): Promise<MountType> {
    const request = new Request(this.appendAPIURL("/v1/sys/internal/ui/mounts/" + mountName), {
      headers: this.getHeaders(),
    });
    const resp = await fetch(request);
    await checkResponse(resp);

    const data = (await resp.json()) as { data: MountType };
    return data.data;
  }

  async getMounts(): Promise<MountsType> {
    const request = new Request(this.appendAPIURL("/v1/sys/internal/ui/mounts"), {
      headers: this.getHeaders(),
    });
    const resp = await fetch(request);
    await checkResponse(resp);

    const data = (await resp.json()) as { data: { secret: MountsType } };
    return data.data.secret;
  }

  async newMount(parms: NewMountParams): Promise<void> {
    const request = new Request(
      this.appendAPIURL(removeDoubleSlash(`/v1/sys/mounts/${parms.name}`)),
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          ...this.getHeaders(),
        },
        body: JSON.stringify(parms),
      },
    );
    const resp = await fetch(request);
    await checkResponse(resp);
  }

  async deleteMount(mountPath: string): Promise<void> {
    const request = new Request(this.appendAPIURL("/v1/sys/mounts/" + mountPath), {
      method: "DELETE",
      headers: this.getHeaders(),
    });
    const resp = await fetch(request);
    await checkResponse(resp);
  }

  async getCapabilitiesPath(path: string | string[]): Promise<CapabilitiesType> {
    if (!Array.isArray(path)) {
      path = [path];
    }

    const request = new Request(this.appendAPIURL("/v1/sys/capabilities-self"), {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        ...this.getHeaders(),
      },
      body: JSON.stringify({
        paths: path,
      }),
    });
    const resp = await fetch(request);
    await checkResponse(resp);

    const data = (await resp.json()) as { capabilities: string[] };
    return data;
  }

  async getCapsPath(path: string | string[]): Promise<string[]> {
    return (await this.getCapabilitiesPath(path)).capabilities;
  }

  async getCapabilities(
    baseMount: string,
    secretPath: string[],
    name: string,
  ): Promise<CapabilitiesType> {
    return await this.getCapabilitiesPath(
      removeDoubleSlash(baseMount + secretPath.join("/") + "/" + name),
    );
  }

  async sealVault(): Promise<void> {
    const request = new Request(this.appendAPIURL("/v1/sys/seal"), {
      method: "PUT",
      headers: this.getHeaders(),
    });
    const resp = await fetch(request);
    await checkResponse(resp);
  }

  async submitUnsealKey(key: string): Promise<void> {
    const request = new Request(this.appendAPIURL("/v1/sys/unseal"), {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        key: key,
      }),
    });
    const resp = await fetch(request);
    await checkResponse(resp);
  }

  async getSealStatus(): Promise<SealStatusType> {
    const request = new Request(this.appendAPIURL("/v1/sys/seal-status"));
    const resp = await fetch(request);
    await checkResponse(resp);

    const data = (await resp.json()) as SealStatusType;
    return data;
  }

  async lookupSelf(): Promise<TokenInfo> {
    const request = new Request(this.appendAPIURL("/v1/auth/token/lookup-self"), {
      headers: this.getHeaders(),
    });
    const resp = await fetch(request);
    await checkResponse(resp);

    const data = (await resp.json()) as { data: TokenInfo };
    return data.data;
  }

  async renewSelf(): Promise<void> {
    const request = new Request(this.appendAPIURL("/v1/auth/token/renew-self"), {
      method: "POST",
      headers: {
        ...this.getHeaders(),
        "Content-Type": "application/json",
      },
      body: JSON.stringify({}),
    });
    const resp = await fetch(request);
    await checkResponse(resp);
  }

  async createOrUpdatePolicy(name: string, policy_data: string): Promise<void> {
    const request = new Request(this.appendAPIURL("/v1/sys/policies/acl/" + name), {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        ...this.getHeaders(),
      },
      body: JSON.stringify({ policy: policy_data }, null, 0),
    });

    const resp = await fetch(request);
    await checkResponse(resp);
  }

  async deletePolicy(name: string): Promise<void> {
    const request = new Request(this.appendAPIURL("/v1/sys/policies/acl/" + name), {
      method: "DELETE",
      headers: this.getHeaders(),
    });
    const resp = await fetch(request);
    await checkResponse(resp);
  }

  async getPolicies(): Promise<string[]> {
    const request = new Request(this.appendAPIURL("/v1/sys/policies/acl?list=true"), {
      headers: this.getHeaders(),
    });
    const resp = await fetch(request);
    await checkResponse(resp);

    const data = (await resp.json()) as { data: { keys: string[] } };
    return data.data.keys;
  }

  async getPolicy(name: string): Promise<string> {
    const request = new Request(this.appendAPIURL("/v1/sys/policies/acl/" + name), {
      headers: this.getHeaders(),
    });
    const resp = await fetch(request);
    await checkResponse(resp);

    const data = (await resp.json()) as { data: { policy: string } };
    return data.data.policy;
  }

  async createOrUpdateSecret(
    baseMount: string,
    secretPath: string[],
    name: string,
    data: Record<string, unknown>,
  ): Promise<void> {
    let secretURL = "";
    let APIData = {};

    const mountInfo = await this.getMount(baseMount);
    if (mountInfo.options.version == "2") {
      secretURL = `/v1/${baseMount}/data/${secretPath.join("/")}/${name}`;
      APIData = { data: data };
    } else {
      secretURL = `/v1/${baseMount}/${secretPath.join("/")}/${name}`;
      APIData = data;
    }

    secretURL = removeDoubleSlash(secretURL).replace(/\/$/, "");
    const request = new Request(this.appendAPIURL(secretURL), {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        ...this.getHeaders(),
      },
      body: JSON.stringify(APIData, null, 0),
    });
    const resp = await fetch(request);
    await checkResponse(resp);
  }

  async deleteSecret(
    baseMount: string,
    secretPath: string[],
    name: string,
    version = "null",
  ): Promise<void> {
    let secretURL = "";

    let request: Request;

    const mountInfo = await this.getMount(baseMount);
    const mountVersion = mountInfo.options.version;

    if (mountVersion == "2" && version != "null") {
      secretURL = `/v1/${baseMount}/delete/${secretPath.join("/")}/${name}`;
      secretURL = removeDoubleSlash(secretURL).replace(/\/$/, "");
      request = new Request(this.appendAPIURL(secretURL), {
        method: "POST",
        headers: {
          ...this.getHeaders(),
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ versions: [version] }),
      });
    } else {
      if (mountVersion == "2") {
        secretURL = `/v1/${baseMount}/metadata/${secretPath.join("/")}/${name}`;
      } else {
        secretURL = `/v1/${baseMount}/${secretPath.join("/")}/${name}`;
      }
      secretURL = removeDoubleSlash(secretURL).replace(/\/$/, "");
      request = new Request(this.appendAPIURL(secretURL), {
        method: "DELETE",
        headers: this.getHeaders(),
      });
    }
    const resp = await fetch(request);
    await checkResponse(resp);
  }

  async undeleteSecret(
    baseMount: string,
    secretPath: string[],
    name: string,
    version = "null",
  ): Promise<void> {
    let secretURL = `/v1/${baseMount}/undelete/${secretPath.join("/")}/${name}`;
    secretURL = removeDoubleSlash(secretURL).replace(/\/$/, "");
    if (version == "null") {
      const meta = await this.getSecretMetadata(baseMount, secretPath, name);
      const versions = getObjectKeys(meta.versions);
      version = String(versions[versions.length - 1]);
    }

    const request = new Request(this.appendAPIURL(secretURL), {
      method: "POST",
      headers: {
        ...this.getHeaders(),
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ versions: [version] }),
    });
    const resp = await fetch(request);
    await checkResponse(resp);
  }

  async getSecretKV1(
    baseMount: string,
    secretPath: string[],
    name: string,
  ): Promise<Record<string, unknown>> {
    const request = new Request(
      this.appendAPIURL(`/v1/${baseMount}/${secretPath.join("/")}/${name}`),
      {
        headers: this.getHeaders(),
      },
    );

    const resp = await fetch(request);
    await checkResponse(resp);

    const data = (await resp.json()) as unknown;

    return (data as { data: Record<string, unknown> }).data;
  }

  async getSecretKV2(
    baseMount: string,
    secretPath: string[],
    name: string,
    version = "null",
  ): Promise<Record<string, unknown>> {
    let secretURL = "";

    secretURL = `/v1/${baseMount}/data/${secretPath.join("/")}/${name}`;
    if (version != "null") secretURL += `?version=${version}`;

    const request = new Request(this.appendAPIURL(secretURL), {
      headers: this.getHeaders(),
    });

    const resp = await fetch(request);
    await checkResponse(resp);

    const data = (await resp.json()) as unknown;
    return (data as { data: { data: Record<string, unknown> } }).data.data;
  }

  async getSecret(
    baseMount: string,
    secretPath: string[],
    name: string,
    version = "null",
  ): Promise<Record<string, unknown>> {
    const mountInfo = await this.getMount(baseMount);
    if (mountInfo.options.version == "2") {
      return await this.getSecretKV2(baseMount, secretPath, name, version);
    } else {
      return await this.getSecretKV1(baseMount, secretPath, name);
    }
  }

  async getSecretMetadata(
    baseMount: string,
    secretPath: string[],
    name: string,
  ): Promise<SecretMetadataType> {
    const request = new Request(
      this.appendAPIURL(`/v1/${baseMount}/metadata/${secretPath.join("")}/${name}`),
      {
        headers: this.getHeaders(),
      },
    );

    const resp = await fetch(request);
    await checkResponse(resp);

    const data = (await resp.json()) as { data: SecretMetadataType };
    return data.data;
  }

  async getSecrets(baseMount: string, secretPath: string[]): Promise<string[]> {
    let secretURL = "";

    const mountInfo = await this.getMount(baseMount);
    if (mountInfo.options.version == "2") {
      secretURL = `/v1/${baseMount}/metadata/${secretPath.join("/")}?list=true`;
    } else {
      secretURL = `/v1/${baseMount}/${secretPath.join("/")}?list=true`;
    }
    const request = new Request(this.appendAPIURL(secretURL), {
      headers: this.getHeaders(),
    });

    const resp = await fetch(request);
    await checkResponse(resp);

    const data = (await resp.json()) as { data: { keys: string[] } };
    return data.data.keys;
  }

  async addNewTOTP(baseMount: string, parms: NewTOTPData): Promise<NewTOTPResp> {
    const request = new Request(
      this.appendAPIURL(removeDoubleSlash(`/v1/${baseMount}/keys/${parms.name}`)),
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          ...this.getHeaders(),
        },
        body: JSON.stringify(parms),
      },
    );
    const resp = await fetch(request);
    await checkResponse(resp);
    const data = (await resp.json()) as { data: NewTOTPResp };
    return data.data;
  }

  async deleteTOTP(baseMount: string, name: string): Promise<void> {
    const request = new Request(this.appendAPIURL(`/v1/${baseMount}/keys/${name}`), {
      method: "DELETE",
      headers: this.getHeaders(),
    });
    const resp = await fetch(request);
    await checkResponse(resp);
  }

  async getTOTPCode(baseMount: string, name: string): Promise<string> {
    const request = new Request(this.appendAPIURL(`/v1/${baseMount}/code/${name}`), {
      headers: this.getHeaders(),
    });

    const resp = await fetch(request);
    await checkResponse(resp);

    const data = (await resp.json()) as { data: { code: string } };
    return data.data.code;
  }

  async getTOTPKeys(baseMount: string): Promise<string[]> {
    const request = new Request(this.appendAPIURL(`/v1/${baseMount}/keys?list=true`), {
      headers: this.getHeaders(),
    });

    const resp = await fetch(request);
    await checkResponse(resp);

    const data = (await resp.json()) as { data: { keys: string[] } };
    return data.data.keys;
  }

  async getTransitKey(baseMount: string, name: string): Promise<TransitKeyType> {
    const request = new Request(this.appendAPIURL(`/v1/${baseMount}/keys/${name}`), {
      headers: this.getHeaders(),
    });

    const resp = await fetch(request);
    await checkResponse(resp);

    const data = (await resp.json()) as { data: TransitKeyType };
    return data.data;
  }

  async getTransitKeys(baseMount: string): Promise<string[]> {
    const request = new Request(this.appendAPIURL(`/v1/${baseMount}/keys?list=true`), {
      headers: this.getHeaders(),
    });

    const resp = await fetch(request);
    await checkResponse(resp);

    const data = (await resp.json()) as { data: { keys: string[] } };
    return data.data.keys;
  }

  async newTransitKey(baseMount: string, parms: { name: string; type: string }): Promise<void> {
    const request = new Request(
      this.appendAPIURL(removeDoubleSlash(`/v1/${baseMount}/keys/${parms.name}`)),
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          ...this.getHeaders(),
        },
        body: JSON.stringify(parms),
      },
    );
    const resp = await fetch(request);
    await checkResponse(resp);
  }

  async transitDecrypt(
    baseMount: string,
    name: string,
    payload: DecryptionPayload,
  ): Promise<DecryptionResult> {
    const request = new Request(
      this.appendAPIURL(removeDoubleSlash(`/v1/${baseMount}/decrypt/${name}`)),
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          ...this.getHeaders(),
        },
        body: JSON.stringify(payload),
      },
    );

    const resp = await fetch(request);
    await checkResponse(resp);

    const data = (await resp.json()) as {
      data?: DecryptionResult;
    };

    return data.data;
  }

  async transitEncrypt(
    baseMount: string,
    name: string,
    payload: EncryptionPayload,
  ): Promise<EncryptionResult> {
    const request = new Request(
      this.appendAPIURL(removeDoubleSlash(`/v1/${baseMount}/encrypt/${name}`)),
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          ...this.getHeaders(),
        },
        body: JSON.stringify(payload),
      },
    );

    const resp = await fetch(request);
    await checkResponse(resp);

    const data = (await resp.json()) as {
      data?: EncryptionResult;
    };

    return data.data;
  }

  async transitRewrap(
    baseMount: string,
    name: string,
    payload: RewrapPayload,
  ): Promise<RewrapResult> {
    const request = new Request(
      this.appendAPIURL(removeDoubleSlash(`/v1/${baseMount}/rewrap/${name}`)),
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          ...this.getHeaders(),
        },
        body: JSON.stringify(payload),
      },
    );

    const resp = await fetch(request);
    await checkResponse(resp);

    const data = (await resp.json()) as {
      data?: RewrapResult;
    };

    return data.data;
  }
}