import * as fs from 'fs';
import * as path from 'path';
import { Project, SourceCode } from 'projen';
import { snakeCase } from 'snake-case';

/**
 * Generate a source file for the `ApiResource` module, based on information about API
 * resources generated by running `kubectl api-resources -o wide`.
 *
 * We do this because `kubectl api-resources` doesn't support JSON output
 * formatting, and at the time of writing, parsing this command output seemed
 * simpler than extracting information from the OpenAPI schema.
 */
export function generateApiResources(project: Project, sourcePath: string, outputPath: string) {
  const resourceTypes = parseApiResources(sourcePath);
  const ts = new SourceCode(project, outputPath);
  if (ts.marker) {
    ts.line(`// ${ts.marker}`);
  }
  ts.line();
  ts.line('/**');
  ts.line(' * Represents a resource or collection of resources.');
  ts.line(' */');
  ts.open('export interface IApiResource {');
  ts.line('/**');
  ts.line(' * The group portion of the API version (e.g. `authorization.k8s.io`).');
  ts.line(' */');
  ts.line('readonly apiGroup: string;');
  ts.line();
  ts.line('/**');
  ts.line(' * The name of a resource type as it appears in the relevant API endpoint.');
  ts.line(' * @example - "pods" or "pods/log"');
  ts.line(' * @see https://kubernetes.io/docs/reference/access-authn-authz/rbac/#referring-to-resources');
  ts.line(' */');
  ts.line('readonly resourceType: string;');
  ts.line();
  ts.line('/**');
  ts.line(' * The unique, namespace-global, name of an object inside the Kubernetes cluster.');
  ts.line(' *');
  ts.line(' * If this is omitted, the ApiResource should represent all objects of the given type.');
  ts.line(' */');
  ts.line('readonly resourceName?: string;');
  ts.close('}');

  ts.line('/**');
  ts.line(' * An API Endpoint can either be a resource descriptor (e.g /pods)');
  ts.line(' * or a non resource url (e.g /healthz). It must be one or the other, and not both.');
  ts.line(' */');
  ts.open('export interface IApiEndpoint {');
  ts.line('');
  ts.line('/**');
  ts.line(' * Return the IApiResource this object represents.');
  ts.line(' */');
  ts.line('asApiResource(): IApiResource | undefined;');
  ts.line('');
  ts.line('/**');
  ts.line(' * Return the non resource url this object represents.');
  ts.line(' */');
  ts.line('asNonApiResource(): string | undefined;');
  ts.line('');
  ts.close('}');

  ts.line();
  ts.line('/**');
  ts.line(' * Options for `ApiResource`.');
  ts.line(' */');
  ts.open('export interface ApiResourceOptions {');
  ts.line('/**');
  ts.line(' * The group portion of the API version (e.g. `authorization.k8s.io`).');
  ts.line(' */');
  ts.line('readonly apiGroup: string;');
  ts.line();
  ts.line('/**');
  ts.line(' * The name of the resource type as it appears in the relevant API endpoint.');
  ts.line(' * @example - "pods" or "pods/log"');
  ts.line(' * @see https://kubernetes.io/docs/reference/access-authn-authz/rbac/#referring-to-resources');
  ts.line(' */');
  ts.line('readonly resourceType: string;');
  ts.close('}');
  ts.line();
  ts.line('/**');
  ts.line(' * Represents information about an API resource type.');
  ts.line(' */');
  ts.open('export class ApiResource implements IApiResource, IApiEndpoint {');

  for (const resource of resourceTypes) {
    const typeName = normalizeTypeName(resource.kind);
    let memberName = snakeCase(typeName.replace(/[^a-z0-9]/gi, '_')).split('_').filter(x => x).join('_').toUpperCase();

    // Pluralize the resource name -- we strip some characters off of memberName
    // in order to handle some weird english plurals, e.g.
    // "CSI_STORAGE_CAPACITY" -> "CSI_STORAGE_CAPACITIES"
    const pluralSuffix = resource.name.substring(resource.kind.length - 1).toUpperCase();
    memberName = memberName.slice(0, -1) + pluralSuffix;

    const apiGroups = resource.apiVersions.map(parseApiGroup);

    ts.line('/**');
    ts.line(` * API resource information for ${resource.kind}.`);
    ts.line(' */');
    ts.open(`public static readonly ${memberName} = new ApiResource({`);
    ts.line(`apiGroup: '${apiGroups[0]}',`);
    ts.line(`resourceType: '${resource.name}',`);
    ts.close('});');
    ts.line();
  }

  ts.line('/**');
  ts.line(' * API resource information for a custom resource type.');
  ts.line(' */');
  ts.open('public static custom(options: ApiResourceOptions): ApiResource {');
  ts.line('return new ApiResource(options);');
  ts.close('};');
  ts.line();
  ts.line('/**');
  ts.line(' * The group portion of the API version (e.g. `authorization.k8s.io`).');
  ts.line(' */');
  ts.line('public readonly apiGroup: string;');
  ts.line();
  ts.line('/**');
  ts.line(' * The name of the resource type as it appears in the relevant API endpoint.');
  ts.line(' * @example - "pods" or "pods/log"');
  ts.line(' * @see https://kubernetes.io/docs/reference/access-authn-authz/rbac/#referring-to-resources');
  ts.line(' */');
  ts.line('public readonly resourceType: string;');
  ts.line();
  ts.open('public asApiResource(): IApiResource | undefined {');
  ts.line('return this;');
  ts.close('}');
  ts.line('');
  ts.open('public asNonApiResource(): string | undefined {');
  ts.line('return undefined;');
  ts.close('}');
  ts.open('private constructor(options: ApiResourceOptions) {');
  ts.line('this.apiGroup = options.apiGroup;');
  ts.line('this.resourceType = options.resourceType;');
  ts.close('}');
  ts.close('}');
  ts.line();

  ts.line('/**');
  ts.line(' * Factory for creating non api resources.');
  ts.line(' */');
  ts.open('export class NonApiResource implements IApiEndpoint {');
  ts.line('');
  ts.open('public static of(url: string): NonApiResource {');
  ts.line('return new NonApiResource(url);');
  ts.close('}');
  ts.line('');
  ts.line('private constructor(private readonly nonResourceUrl: string) {};');
  ts.line();
  ts.open('public asApiResource(): IApiResource | undefined {');
  ts.line('return undefined;');
  ts.close('}');
  ts.line('');
  ts.open('public asNonApiResource(): string | undefined {');
  ts.line();
  ts.line('return this.nonResourceUrl;');
  ts.close('}');
  ts.close('}');

}

/**
 * Extract structured API resource information from the textual output of the
 * `kubectl api-resources -o wide` command.
 */
function parseApiResources(filename: string): Array<ApiResourceEntry> {
  const fileContents = fs.readFileSync(path.join(filename)).toString();
  const lines = fileContents.split('\n');
  const header = lines[0];
  const dataLines = lines.slice(1);
  const columns = calculateColumnMetadata(header);

  const apiResources = new Array<ApiResourceEntry>();

  for (const line of dataLines) {
    const entry: any = {};
    if (line == '') {
      continue;
    }

    for (const column of columns) {
      const value = line.slice(column.start, column.end).trim();
      entry[column.title.toLowerCase()] = value;
    }

    const massaged = sanitizeData(entry);
    apiResources.push(massaged);
  }

  combineResources(apiResources);

  return apiResources;
}

/**
 * Sanitize data that has been parsed from `kubectl api-resources -o wide`
 * from string types into JavaScript values like booleans and arrays.
 */
function sanitizeData(entry: any): ApiResourceEntry {
  let shortnames = entry.shortnames.split(',');
  shortnames = shortnames[0].length === 0 ? [] : shortnames;

  return {
    name: entry.name,
    shortnames,
    apiVersions: [entry.apiversion],
    namespaced: Boolean(entry.namespaced),
    kind: entry.kind,
    verbs: entry.verbs.slice(1, -1).split(' '),
  };
}

/**
 * Sometimes resources have multiple API versions (e.g. events is listed under
 * "v1" and "events.k8s.io/v1"), so we combine them together.
 */
function combineResources(resources: ApiResourceEntry[]) {
  let i = 0;
  while (i < resources.length) {
    let didCombine = false;
    for (let j = i + 1; j < resources.length; j++) {
      if (resources[i].kind === resources[j].kind
        && resources[i].name === resources[j].name
        && resources[i].namespaced === resources[j].namespaced
      ) {
        const combined: ApiResourceEntry = {
          kind: resources[i].kind,
          name: resources[i].name,
          apiVersions: Array.from(new Set(
            [...resources[i].apiVersions, ...resources[j].apiVersions],
          )),
          namespaced: resources[i].namespaced,
          shortnames: Array.from(new Set(
            [...resources[i].shortnames, ...resources[j].shortnames],
          )),
          verbs: Array.from(new Set(
            [...resources[i].verbs, ...resources[j].verbs],
          )),
        };
        resources[i] = combined;
        resources.splice(j, 1);
        didCombine = true;
        break;
      }
    }

    if (!didCombine) {
      i++;
    }
  }
}

interface ApiResourceEntry {
  readonly name: string;
  readonly shortnames: string[];
  readonly apiVersions: string[];
  readonly namespaced: boolean;
  readonly kind: string;
  readonly verbs: string[];
}

interface Column {
  readonly title: string;
  readonly start: number;
  readonly end?: number; // last column does not have an end index
}

/**
 * Given a string of this form:
 *
 *          "NAME      SHORTNAMES  APIVERSION"
 *  indices: 0         10          22        31
 *
 * we return an array like:
 *
 * [{ title: "NAME", start: 0, end: 10 },
 *  { title: "SHORTNAMES", start: 10, end: 22 },
 *  { title: "APIVERSION", start: 22, end: 31 }]
 */
function calculateColumnMetadata(header: string): Array<Column> {
  const headerRegex = /([A-Z]+)(\s+)([A-Z]+)(\s+)([A-Z]+)(\s+)([A-Z]+)(\s+)([A-Z]+)(\s+)([A-Z]+)/;
  const matches = headerRegex.exec(header)!;

  const columns = new Array<Column>();
  let currIndex = 0;

  for (let matchIdx = 1; matchIdx < matches.length - 1; matchIdx += 2) {
    const start = currIndex;
    const title = matches[matchIdx];
    currIndex += matches[matchIdx].length;
    currIndex += matches[matchIdx + 1].length;
    const end = currIndex;

    columns.push({ title, start, end });
  }

  // add last column as special case
  columns.push({ title: matches[matches.length - 1], start: currIndex });

  return columns;
}

/**
 * Convert all-caps acronyms (e.g. "VPC", "FooBARZooFIGoo") to pascal case
 * (e.g. "Vpc", "FooBarZooFiGoo").
 *
 * note: code borrowed from json2jsii
 */
function normalizeTypeName(typeName: string) {
  // start with the full string and then use the regex to match all-caps sequences.
  const re = /([A-Z]+)(?:[^a-z]|$)/g;
  let result = typeName;
  let m;
  do {
    m = re.exec(typeName);
    if (m) {
      const before = result.slice(0, m.index); // all the text before the sequence
      const cap = m[1]; // group #1 matches the all-caps sequence we are after
      const pascal = cap[0] + cap.slice(1).toLowerCase(); // convert to pascal case by lowercasing all but the first char
      const after = result.slice(m.index + pascal.length); // all the text after the sequence
      result = before + pascal + after; // concat
    }
  } while (m);

  result = result.replace(/^\S/, result[0]?.toUpperCase()); // ensure first letter is capitalized
  return result;
}

/**
 * Parses the apiGroup from an apiVersion.
 * @example "admissionregistration.k8s.io/v1" => "admissionregistration.k8s.io"
 */
function parseApiGroup(apiVersion: string) {
  const v = apiVersion.split('/');

  // no group means it's in the core group
  // https://kubernetes.io/docs/reference/using-api/api-overview/#api-groups
  if (v.length === 1) {
    return '';
  }

  if (v.length === 2) {
    return v[0];
  }

  throw new Error(`invalid apiVersion ${apiVersion}, expecting GROUP/VERSION. See https://kubernetes.io/docs/reference/using-api/api-overview/#api-groups`);
}
