import {Features} from 'app/common/Features';
import {nativeValues} from 'app/gen-server/lib/values';
import * as assert from 'assert';
import {BaseEntity, Column, Connection, Entity, PrimaryGeneratedColumn} from 'typeorm';

/**
 * A summary of features used in 'starter' plans.
 */
export const starterFeatures: Features = {
  workspaces: true,
  // no vanity domain
  maxDocsPerOrg: 10,
  maxSharesPerDoc: 2,
  maxWorkspacesPerOrg: 1
};

/**
 * A summary of features used in 'team' plans.
 */
export const teamFeatures: Features = {
  workspaces: true,
  vanityDomain: true,
  maxSharesPerWorkspace: 0,   // all workspace shares need to be org members.
  maxSharesPerDoc: 2
};

/**
 * A summary of features used in unrestricted grandfathered accounts, and also
 * in some test settings.
 */
export const grandfatherFeatures: Features = {
  workspaces: true,
  vanityDomain: true,
};

export const suspendedFeatures: Features = {
  workspaces: true,
  vanityDomain: true,
  readOnlyDocs: true,
  // clamp down on new docs/workspaces/shares
  maxDocsPerOrg: 0,
  maxSharesPerDoc: 0,
  maxWorkspacesPerOrg: 0,
};

/**
 * Basic fields needed for products supported by Grist.
 */
export interface IProduct {
  name: string;
  features: Features;
}

/**
 *
 * Products are a bundle of enabled features.  Most products in
 * Grist correspond to products in stripe.  The correspondence is
 * established by a gristProduct metadata field on stripe plans.
 *
 * In addition, there are the following products in Grist that don't
 * exist in stripe:
 *   - The product named 'Free'.  This is a product used for organizations
 *     created prior to the billing system being set up.
 *   - The product named 'stub'.  This is product assigned to new
 *     organizations that should not be usable until a paid plan
 *     is set up for them.
 *
 * TODO: change capitalization of name of grandfather product.
 *
 */
const PRODUCTS: IProduct[] = [
  // This is a product for grandfathered accounts/orgs.
  {
    name: 'Free',
    features: grandfatherFeatures,
  },

  // This is a product for newly created accounts/orgs.
  {
    name: 'stub',
    features: {},
  },

  // These are products set up in stripe.
  {
    name: 'starter',
    features: starterFeatures,
  },
  {
    name: 'professional',  // deprecated, can be removed once no longer referred to in stripe.
    features: teamFeatures,
  },
  {
    name: 'team',
    features: teamFeatures,
  },

  // This is a product for a team site that is no longer in good standing, but isn't yet
  // to be removed / deactivated entirely.
  {
    name: 'suspended',
    features: suspendedFeatures,
  },
];

/**
 * Get names of products for different situations.
 */
export function getDefaultProductNames() {
  return {
    personal: 'starter',  // Personal site start off on a functional plan.
    teamInitial: 'stub',  // Team site starts off on a limited plan, requiring subscription.
    teamCancel: 'suspended',  // Team site that has been 'turned off'.
    team: 'team',         // Functional team site.
 };
}

/**
 * A Grist product.  Corresponds to a set of enabled features and a choice of limits.
 */
@Entity({name: 'products'})
export class Product extends BaseEntity {
  @PrimaryGeneratedColumn()
  public id: number;

  @Column()
  public name: string;

  @Column({type: nativeValues.jsonEntityType})
  public features: Features;
}

/**
 * Make sure the products defined for the current stripe setup are
 * in the database and up to date.  Other products in the database
 * are untouched.
 *
 * If `apply` is set, the products are changed in the db, otherwise
 * the are left unchanged.  A summary of affected products is returned.
 */
export async function synchronizeProducts(connection: Connection, apply: boolean): Promise<string[]> {
  try {
    await connection.query('select name, features, stripe_product_id from products limit 1');
  } catch (e) {
    // No usable products table, do not try to synchronize.
    return [];
  }
  const changingProducts: string[] = [];
  await connection.transaction(async transaction => {
    const desiredProducts = new Map(PRODUCTS.map(p => [p.name, p]));
    const existingProducts = new Map((await transaction.find(Product))
                                     .map(p => [p.name, p]));
    for (const product of desiredProducts.values()) {
      if (existingProducts.has(product.name)) {
        const p = existingProducts.get(product.name)!;
        try {
          assert.deepStrictEqual(p.features, product.features);
        } catch (e) {
          if (apply) {
            p.features = product.features;
            await transaction.save(p);
          }
          changingProducts.push(p.name);
        }
      } else {
        if (apply) {
          const p = new Product();
          p.name = product.name;
          p.features = product.features;
          await transaction.save(p);
        }
        changingProducts.push(product.name);
      }
    }
  });
  return changingProducts;
}
