import i18n from "i18next";
import {
  accountBillingPortalUrl,
  accountBillingSubscriptionUrl,
  accountPasswordUrl,
  accountPhoneUrl,
  accountPhoneVerifyUrl,
  accountReservationSingleUrl,
  accountReservationUrl,
  accountSettingsUrl,
  accountSubscriptionUrl,
  accountTokenUrl,
  accountUrl,
  maybeWithBearerAuth,
  tiersUrl,
  withBasicAuth,
  withBearerAuth,
} from "./utils";
import session from "./Session";
import subscriptionManager from "./SubscriptionManager";
import prefs from "./Prefs";
import routes from "../components/routes";
import { fetchOrThrow, UnauthorizedError } from "./errors";

const delayMillis = 45000; // 45 seconds
const intervalMillis = 900000; // 15 minutes

class AccountApi {
  constructor() {
    this.timer = null;
    this.listener = null; // Fired when account is fetched from remote
    this.tiers = null; // Cached
  }

  registerListener(listener) {
    this.listener = listener;
  }

  resetListener() {
    this.listener = null;
  }

  async login(user) {
    const url = accountTokenUrl(config.base_url);
    console.log(`[AccountApi] Checking auth for ${url}`);
    const response = await fetchOrThrow(url, {
      method: "POST",
      headers: withBasicAuth({}, user.username, user.password),
    });
    const json = await response.json(); // May throw SyntaxError
    if (!json.token) {
      throw new Error(`Unexpected server response: Cannot find token`);
    }
    return json.token;
  }

  async logout() {
    const url = accountTokenUrl(config.base_url);
    console.log(`[AccountApi] Logging out from ${url} using token ${session.token()}`);
    await fetchOrThrow(url, {
      method: "DELETE",
      headers: withBearerAuth({}, session.token()),
    });
  }

  async create(username, password) {
    const url = accountUrl(config.base_url);
    const body = JSON.stringify({
      username,
      password,
    });
    console.log(`[AccountApi] Creating user account ${url}`);
    await fetchOrThrow(url, {
      method: "POST",
      body,
    });
  }

  async get() {
    const url = accountUrl(config.base_url);
    console.log(`[AccountApi] Fetching user account ${url}`);
    const response = await fetchOrThrow(url, {
      headers: maybeWithBearerAuth({}, session.token()), // GET /v1/account endpoint can be called by anonymous
    });
    const account = await response.json(); // May throw SyntaxError
    console.log(`[AccountApi] Account`, account);
    if (this.listener) {
      this.listener(account);
    }
    return account;
  }

  async delete(password) {
    const url = accountUrl(config.base_url);
    console.log(`[AccountApi] Deleting user account ${url}`);
    await fetchOrThrow(url, {
      method: "DELETE",
      headers: withBearerAuth({}, session.token()),
      body: JSON.stringify({
        password,
      }),
    });
  }

  async changePassword(currentPassword, newPassword) {
    const url = accountPasswordUrl(config.base_url);
    console.log(`[AccountApi] Changing account password ${url}`);
    await fetchOrThrow(url, {
      method: "POST",
      headers: withBearerAuth({}, session.token()),
      body: JSON.stringify({
        password: currentPassword,
        new_password: newPassword,
      }),
    });
  }

  async createToken(label, expires) {
    const url = accountTokenUrl(config.base_url);
    const body = {
      label,
      expires: expires > 0 ? Math.floor(Date.now() / 1000) + expires : 0,
    };
    console.log(`[AccountApi] Creating user access token ${url}`);
    await fetchOrThrow(url, {
      method: "POST",
      headers: withBearerAuth({}, session.token()),
      body: JSON.stringify(body),
    });
  }

  async updateToken(token, label, expires) {
    const url = accountTokenUrl(config.base_url);
    const body = {
      token,
      label,
    };
    if (expires > 0) {
      body.expires = Math.floor(Date.now() / 1000) + expires;
    }
    console.log(`[AccountApi] Creating user access token ${url}`);
    await fetchOrThrow(url, {
      method: "PATCH",
      headers: withBearerAuth({}, session.token()),
      body: JSON.stringify(body),
    });
  }

  async extendToken() {
    const url = accountTokenUrl(config.base_url);
    console.log(`[AccountApi] Extending user access token ${url}`);
    await fetchOrThrow(url, {
      method: "PATCH",
      headers: withBearerAuth({}, session.token()),
    });
  }

  async deleteToken(token) {
    const url = accountTokenUrl(config.base_url);
    console.log(`[AccountApi] Deleting user access token ${url}`);
    await fetchOrThrow(url, {
      method: "DELETE",
      headers: withBearerAuth({ "X-Token": token }, session.token()),
    });
  }

  async updateSettings(payload) {
    const url = accountSettingsUrl(config.base_url);
    const body = JSON.stringify(payload);
    console.log(`[AccountApi] Updating user account ${url}: ${body}`);
    await fetchOrThrow(url, {
      method: "PATCH",
      headers: withBearerAuth({}, session.token()),
      body,
    });
  }

  async addSubscription(baseUrl, topic) {
    const url = accountSubscriptionUrl(config.base_url);
    const body = JSON.stringify({
      base_url: baseUrl,
      topic,
    });
    console.log(`[AccountApi] Adding user subscription ${url}: ${body}`);
    const response = await fetchOrThrow(url, {
      method: "POST",
      headers: withBearerAuth({}, session.token()),
      body,
    });
    const subscription = await response.json(); // May throw SyntaxError
    console.log(`[AccountApi] Subscription`, subscription);
    return subscription;
  }

  async updateSubscription(baseUrl, topic, payload) {
    const url = accountSubscriptionUrl(config.base_url);
    const body = JSON.stringify({
      base_url: baseUrl,
      topic,
      ...payload,
    });
    console.log(`[AccountApi] Updating user subscription ${url}: ${body}`);
    const response = await fetchOrThrow(url, {
      method: "PATCH",
      headers: withBearerAuth({}, session.token()),
      body,
    });
    const subscription = await response.json(); // May throw SyntaxError
    console.log(`[AccountApi] Subscription`, subscription);
    return subscription;
  }

  async deleteSubscription(baseUrl, topic) {
    const url = accountSubscriptionUrl(config.base_url);
    console.log(`[AccountApi] Removing user subscription ${url}`);
    const headers = {
      "X-BaseURL": baseUrl,
      "X-Topic": topic,
    };
    await fetchOrThrow(url, {
      method: "DELETE",
      headers: withBearerAuth(headers, session.token()),
    });
  }

  async upsertReservation(topic, everyone) {
    const url = accountReservationUrl(config.base_url);
    console.log(`[AccountApi] Upserting user access to topic ${topic}, everyone=${everyone}`);
    await fetchOrThrow(url, {
      method: "POST",
      headers: withBearerAuth({}, session.token()),
      body: JSON.stringify({
        topic,
        everyone,
      }),
    });
  }

  async deleteReservation(topic, deleteMessages) {
    const url = accountReservationSingleUrl(config.base_url, topic);
    console.log(`[AccountApi] Removing topic reservation ${url}`);
    const headers = {
      "X-Delete-Messages": deleteMessages ? "true" : "false",
    };
    await fetchOrThrow(url, {
      method: "DELETE",
      headers: withBearerAuth(headers, session.token()),
    });
  }

  async billingTiers() {
    if (this.tiers) {
      return this.tiers;
    }
    const url = tiersUrl(config.base_url);
    console.log(`[AccountApi] Fetching billing tiers`);
    const response = await fetchOrThrow(url); // No auth needed!
    this.tiers = await response.json(); // May throw SyntaxError
    return this.tiers;
  }

  async createBillingSubscription(tier, interval) {
    console.log(`[AccountApi] Creating billing subscription with ${tier} and interval ${interval}`);
    return this.upsertBillingSubscription("POST", tier, interval);
  }

  async updateBillingSubscription(tier, interval) {
    console.log(`[AccountApi] Updating billing subscription with ${tier} and interval ${interval}`);
    return this.upsertBillingSubscription("PUT", tier, interval);
  }

  async upsertBillingSubscription(method, tier, interval) {
    const url = accountBillingSubscriptionUrl(config.base_url);
    const response = await fetchOrThrow(url, {
      method,
      headers: withBearerAuth({}, session.token()),
      body: JSON.stringify({
        tier,
        interval,
      }),
    });
    return response.json(); // May throw SyntaxError
  }

  async deleteBillingSubscription() {
    const url = accountBillingSubscriptionUrl(config.base_url);
    console.log(`[AccountApi] Cancelling billing subscription`);
    await fetchOrThrow(url, {
      method: "DELETE",
      headers: withBearerAuth({}, session.token()),
    });
  }

  async createBillingPortalSession() {
    const url = accountBillingPortalUrl(config.base_url);
    console.log(`[AccountApi] Creating billing portal session`);
    const response = await fetchOrThrow(url, {
      method: "POST",
      headers: withBearerAuth({}, session.token()),
    });
    return response.json(); // May throw SyntaxError
  }

  async verifyPhoneNumber(phoneNumber, channel) {
    const url = accountPhoneVerifyUrl(config.base_url);
    console.log(`[AccountApi] Sending phone verification ${url}`);
    await fetchOrThrow(url, {
      method: "PUT",
      headers: withBearerAuth({}, session.token()),
      body: JSON.stringify({
        number: phoneNumber,
        channel,
      }),
    });
  }

  async addPhoneNumber(phoneNumber, code) {
    const url = accountPhoneUrl(config.base_url);
    console.log(`[AccountApi] Adding phone number with verification code ${url}`);
    await fetchOrThrow(url, {
      method: "PUT",
      headers: withBearerAuth({}, session.token()),
      body: JSON.stringify({
        number: phoneNumber,
        code,
      }),
    });
  }

  async deletePhoneNumber(phoneNumber) {
    const url = accountPhoneUrl(config.base_url);
    console.log(`[AccountApi] Deleting phone number ${url}`);
    await fetchOrThrow(url, {
      method: "DELETE",
      headers: withBearerAuth({}, session.token()),
      body: JSON.stringify({
        number: phoneNumber,
      }),
    });
  }

  async sync() {
    try {
      if (!session.token()) {
        return null;
      }
      console.log(`[AccountApi] Syncing account`);
      const account = await this.get();
      if (account.language) {
        await i18n.changeLanguage(account.language);
      }
      if (account.notification) {
        if (account.notification.sound) {
          await prefs.setSound(account.notification.sound);
        }
        if (account.notification.delete_after) {
          await prefs.setDeleteAfter(account.notification.delete_after);
        }
        if (account.notification.min_priority) {
          await prefs.setMinPriority(account.notification.min_priority);
        }
      }
      if (account.subscriptions) {
        await subscriptionManager.syncFromRemote(account.subscriptions, account.reservations);
      }
      return account;
    } catch (e) {
      console.log(`[AccountApi] Error fetching account`, e);
      if (e instanceof UnauthorizedError) {
        await session.resetAndRedirect(routes.login);
      }
      return undefined;
    }
  }

  startWorker() {
    if (this.timer !== null) {
      return;
    }
    console.log(`[AccountApi] Starting worker`);
    this.timer = setInterval(() => this.runWorker(), intervalMillis);
    setTimeout(() => this.runWorker(), delayMillis);
  }

  stopWorker() {
    clearTimeout(this.timer);
  }

  async runWorker() {
    if (!session.token()) {
      return;
    }
    console.log(`[AccountApi] Extending user access token`);
    try {
      await this.extendToken();
    } catch (e) {
      console.log(`[AccountApi] Error extending user access token`, e);
    }
  }
}

// Maps to user.Role in user/types.go
export const Role = {
  ADMIN: "admin",
  USER: "user",
};

// Maps to server.visitorLimitBasis in server/visitor.go
export const LimitBasis = {
  IP: "ip",
  TIER: "tier",
};

// Maps to stripe.SubscriptionStatus
export const SubscriptionStatus = {
  ACTIVE: "active",
  PAST_DUE: "past_due",
};

// Maps to stripe.PriceRecurringInterval
export const SubscriptionInterval = {
  MONTH: "month",
  YEAR: "year",
};

// Maps to user.Permission in user/types.go
export const Permission = {
  READ_WRITE: "read-write",
  READ_ONLY: "read-only",
  WRITE_ONLY: "write-only",
  DENY_ALL: "deny-all",
};

const accountApi = new AccountApi();
export default accountApi;
