import * as _ from 'lodash-es';
import * as React from 'react';
import { Helmet } from 'react-helmet';
import { connect } from 'react-redux';
import * as classNames from 'classnames';
import { ActionGroup, Button } from '@patternfly/react-core';
import { sortable } from '@patternfly/react-table';
import { ClusterRoleBindingModel } from '../../models';
import { getQN, k8sCreate, k8sPatch, referenceFor } from '../../module/k8s';
import * as UIActions from '../../actions/ui';
import { MultiListPage, Table, TableRow, TableData } from '../factory';
import { RadioGroup } from '../radio';
import { confirmModal } from '../modals';
import {
  ButtonBar,
  Kebab,
  Firehose,
  ListDropdown,
  MsgBox,
  NsDropdown,
  ResourceKebab,
  ResourceLink,
  ResourceName,
  StatusBox,
  getQueryArgument,
  history,
  kindObj,
  resourceObjPath,
  useAccessReview,
} from '../utils';
import { isSystemRole } from './index';
import { connectToFlags, flagPending } from '../../reducers/features';
import { FLAGS } from '../../const';

const bindingKind = binding => binding.metadata.namespace ? 'RoleBinding' : 'ClusterRoleBinding';

// Split each binding into one row per subject
export const flatten = resources => _.flatMap(resources, resource => {
  const ret = [];

  _.each(resource.data, binding => {
    if (!binding) {
      return undefined;
    }
    if (_.isEmpty(binding.subjects)) {
      const subject = {kind: '-', name: '-'};
      return ret.push(Object.assign({}, binding, {subject}));
    }
    _.each(binding.subjects, (subject, subjectIndex) => {
      ret.push(Object.assign({}, binding, {
        subject,
        subjectIndex,
        rowKey: `${getQN(binding)}|${subject.kind}|${subject.name}${subject.namespace ? `|${subject.namespace}` : ''}`,
      }));
    });
  });

  return ret;
});

const menuActions = ({subjectIndex, subjects}, startImpersonate) => {
  const subject = subjects[subjectIndex];

  const actions = [
    (kind, obj) => ({
      label: `Duplicate ${kind.label}`,
      href: `${resourceObjPath(obj, kind.kind)}/copy?subjectIndex=${subjectIndex}`,
      // Only perform access checks when duplicating cluster role bindings.
      // It's not practical to check namespace role bindings since we don't know what namespace the user will pick in the form.
      accessReview: _.get(obj, 'metadata.namespace') ? null : { group: kind.apiGroup, resource: kind.plural, verb: 'create' },
    }),
    (kind, obj) => ({
      label: `Edit ${kind.label} Subject`,
      href: `${resourceObjPath(obj, kind.kind)}/edit?subjectIndex=${subjectIndex}`,
      accessReview: {
        group: kind.apiGroup,
        resource: kind.plural,
        name: obj.metadata.name,
        namespace: obj.metadata.namespace,
        verb: 'update',
      },
    }),
    subjects.length === 1 ? Kebab.factory.Delete : (kind, binding) => ({
      label: `Delete ${kind.label} Subject`,
      callback: () => confirmModal({
        title: `Delete ${kind.label} Subject`,
        message: `Are you sure you want to delete subject ${subject.name} of type ${subject.kind}?`,
        btnText: 'Delete Subject',
        executeFn: () => k8sPatch(kind, binding, [{op: 'remove', path: `/subjects/${subjectIndex}`}]),
      }),
      accessReview: {
        group: kind.apiGroup,
        resource: kind.plural,
        name: binding.metadata.name,
        namespace: binding.metadata.namespace,
        verb: 'patch',
      },
    }),
  ];

  if (subject.kind === 'User' || subject.kind === 'Group') {
    actions.unshift(() => ({
      label: `Impersonate ${subject.kind} "${subject.name}"`,
      callback: () => startImpersonate(subject.kind, subject.name),
    }));
  }

  return actions;
};

const tableColumnClasses = [
  classNames('col-md-3', 'col-sm-4', 'col-xs-6'),
  classNames('col-md-3', 'col-sm-4', 'hidden-xs'),
  classNames('col-lg-2', 'col-md-3', 'hidden-sm', 'hidden-xs'),
  classNames('col-lg-2', 'hidden-md', 'hidden-sm', 'hidden-xs'),
  classNames('col-lg-2', 'col-md-3', 'col-sm-4', 'col-xs-6'),
  Kebab.columnClass,
];

const RoleBindingsTableHeader = () => {
  return [
    {
      title: 'Name', sortField: 'metadata.name', transforms: [sortable],
      props: { className: tableColumnClasses[0] },
    },
    {
      title: 'Role Ref', sortField: 'roleRef.name', transforms: [sortable],
      props: { className: tableColumnClasses[1] },
    },
    {
      title: 'Subject Kind', sortField: 'subject.kind', transforms: [sortable],
      props: { className: tableColumnClasses[2] },
    },
    {
      title: 'Subject Name', sortField: 'subject.name', transforms: [sortable],
      props: { className: tableColumnClasses[3] },
    },
    {
      title: 'Namespace', sortField: 'metadata.namespace', transforms: [sortable],
      props: { className: tableColumnClasses[4] },
    },
    {
      title: '', props: { className: tableColumnClasses[5] },
    },
  ];
};
RoleBindingsTableHeader.displayName = 'RoleBindingsTableHeader';

export const BindingName = ({binding}) => {
  <ResourceLink kind={bindingKind(binding)} name={binding.metadata.name} namespace={binding.metadata.namespace} className="co-resource-item__resource-name" />;
};

export const BindingKebab = connect(null, {startImpersonate: UIActions.startImpersonate})(
  ({binding, startImpersonate}) => binding.subjects ? <ResourceKebab actions={menuActions(binding, startImpersonate)} kind={bindingKind(binding)} resource={binding} /> : null
);

export const RoleLink = ({binding}) => {
  const kind = binding.roleRef.kind;

  // Cluster Roles have no namespace and for Roles, the Role's namespace matches the Role Binding's namespace
  const ns = kind === 'ClusterRole' ? undefined : binding.metadata.namespace;
  return <ResourceLink kind={kind} name={binding.roleRef.name} namespace={ns} />;
};

const RoleBindingsTableRow = ({obj: binding, index, key, style}) => {
  return (
    <TableRow id={binding.metadata.uid} index={index} trKey={key} style={style}>
      <TableData className={tableColumnClasses[0]}>
        <ResourceLink kind={bindingKind(binding)} name={binding.metadata.name} namespace={binding.metadata.namespace} className="co-resource-item__resource-name" />
      </TableData>
      <TableData className={classNames(tableColumnClasses[1], 'co-break-word')}>
        <RoleLink binding={binding} />
      </TableData>
      <TableData className={classNames(tableColumnClasses[2], 'co-break-word')}>
        {binding.subject.kind}
      </TableData>
      <TableData className={classNames(tableColumnClasses[3], 'co-break-word')}>
        {binding.subject.name}
      </TableData>
      <TableData className={classNames(tableColumnClasses[4], 'co-break-word')}>
        {binding.metadata.namespace ? <ResourceLink kind="Namespace" name={binding.metadata.namespace} /> : 'all'}
      </TableData>
      <TableData className={tableColumnClasses[5]}>
        <BindingKebab binding={binding} />
      </TableData>
    </TableRow>
  );
};
RoleBindingsTableRow.displayName = 'RoleBindingsTableRow';

const EmptyMsg = () => <MsgBox title="No Role Bindings Found" detail="Roles grant access to types of objects in the cluster. Roles are applied to a group or user via a Role Binding." />;

export const BindingsList = props => <Table {...props} aria-label="Role Bindings" EmptyMsg={EmptyMsg} Header={RoleBindingsTableHeader} Row={RoleBindingsTableRow} virtualize />;

export const bindingType = binding => {
  if (!binding) {
    return undefined;
  }
  if (binding.roleRef.name.startsWith('system:')) {
    return 'system';
  }
  return binding.metadata.namespace ? 'namespace' : 'cluster';
};

const roleResources = [
  {kind: 'RoleBinding', namespaced: true},
  {kind: 'ClusterRoleBinding', namespaced: false, optional: true},
];

export const RoleBindingsPage = ({namespace, showTitle=true, mock}) => <MultiListPage
  canCreate={true}
  createButtonText="Create Binding"
  createProps={{to: '/k8s/cluster/rolebindings/~new'}}
  mock={mock}
  filterLabel="by role or subject"
  flatten={flatten}
  label="Role Bindings"
  ListComponent={BindingsList}
  namespace={namespace}
  resources={roleResources}
  rowFilters={[{
    type: 'role-binding-kind',
    selected: ['cluster', 'namespace'],
    reducer: bindingType,
    items: ({ClusterRoleBinding: data}) => {
      const items = [
        {id: 'namespace', title: 'Namespace Role Bindings'},
        {id: 'system', title: 'System Role Bindings'},
      ];
      if (data && data.loaded && !data.loadError) {
        items.unshift({id: 'cluster', title: 'Cluster-wide Role Bindings'});
      }
      return items;
    },
  }]}
  showTitle={showTitle}
  textFilter="role-binding"
  title="Role Bindings"
/>;

const NsRoleDropdown_ = props => {
  const openshiftFlag = props.flags[FLAGS.OPENSHIFT];
  if (flagPending(openshiftFlag)) {
    return null;
  }

  const roleFilter = role => !isSystemRole(role);

  let kinds;
  if (props.fixed) {
    kinds = [props.selectedKeyKind];
  } else if (props.namespace) {
    kinds = ['Role', 'ClusterRole'];
  } else {
    kinds = ['ClusterRole'];
  }
  const resourceForKind = kind => ({ kind, namespace: kind === 'Role' ? props.namespace : null });
  const resources = _.map(kinds, resourceForKind);

  return <ListDropdown
    {...props}
    dataFilter={roleFilter}
    desc="Namespace Roles (Role)"
    resources={resources}
    placeholder="Select role name"
  />;
};
const NsRoleDropdown = connectToFlags(FLAGS.OPENSHIFT)(NsRoleDropdown_);

const ClusterRoleDropdown = props => <ListDropdown
  {...props}
  dataFilter={role => !isSystemRole(role)}
  desc="Cluster-wide Roles (ClusterRole)"
  resources={[{ kind: 'ClusterRole' }]}
  placeholder="Select role name"
/>;

const bindingKinds = [
  {value: 'RoleBinding', title: 'Namespace Role Binding (RoleBinding)', desc: 'Grant the permissions to a user or set of users within the selected namespace.'},
  {value: 'ClusterRoleBinding', title: 'Cluster-wide Role Binding (ClusterRoleBinding)', desc: 'Grant the permissions to a user or set of users at the cluster level and in all namespaces.'},
];
const subjectKinds = [
  {value: 'User', title: 'User'},
  {value: 'Group', title: 'Group'},
  {value: 'ServiceAccount', title: 'Service Account'},
];

const Section = ({label, children}) => <div>
  <div className="co-form-section__label">{label}</div>
  <div className="co-form-subsection">{children}</div>
</div>;

const BaseEditRoleBinding = connect(null, {setActiveNamespace: UIActions.setActiveNamespace})(
  class BaseEditRoleBinding_ extends React.Component {
    constructor(props) {
      super(props);

      this.subjectIndex = props.subjectIndex || 0;

      const existingData = _.pick(props.obj, ['metadata.name', 'metadata.namespace', 'roleRef', 'subjects']);
      existingData.kind = props.kind;
      const data = _.defaultsDeep({}, props.fixed, existingData, {
        apiVersion: 'rbac.authorization.k8s.io/v1',
        kind: 'RoleBinding',
        metadata: {
          name: '',
        },
        roleRef: {
          apiGroup: 'rbac.authorization.k8s.io',
        },
        subjects: [{
          apiGroup: 'rbac.authorization.k8s.io',
          kind: 'User',
          name: '',
        }],
      });
      this.state = {data, inProgress: false};

      this.setKind = this.setKind.bind(this);
      this.setSubject = this.setSubject.bind(this);
      this.save = this.save.bind(this);

      this.setData = patch => this.setState({data: _.defaultsDeep({}, patch, this.state.data)});
      this.changeName = e => this.setData({metadata: {name: e.target.value}});
      this.changeNamespace = namespace => this.setData({metadata: {namespace}});
      this.changeRoleRef = (name, kindId) => this.setData({roleRef: {name, kind: kindId}});
      this.changeSubjectKind = e => this.setSubject({kind: e.target.value});
      this.changeSubjectName = e => this.setSubject({name: e.target.value});
      this.changeSubjectNamespace = namespace => this.setSubject({namespace});
    }

    setKind(e) {
      const kind = e.target.value;
      const patch = {kind};
      if (kind === 'ClusterRoleBinding') {
        patch.metadata = {namespace: null};
      }
      this.setData(patch);
    }

    getSubject() {
      return _.get(this.state.data, `subjects[${this.subjectIndex}]`);
    }

    setSubject(patch) {
      const {kind, name, namespace} = Object.assign({}, this.getSubject(), patch);
      const data = Object.assign({}, this.state.data);
      data.subjects[this.subjectIndex] = kind === 'ServiceAccount' ? {kind, name, namespace} : {apiGroup: 'rbac.authorization.k8s.io', kind, name};
      this.setState({data});
    }

    save(e) {
      e.preventDefault();

      const {kind, metadata, roleRef} = this.state.data;
      const subject = this.getSubject();

      if (!kind || !metadata.name || !roleRef.kind || !roleRef.name || !subject.kind || !subject.name ||
      (kind === 'RoleBinding' && !metadata.namespace) ||
      (subject.kind === 'ServiceAccount') && !subject.namespace) {
        this.setState({error: 'Please complete all fields.'});
        return;
      }

      this.setState({inProgress: true});

      const ko = kindObj(kind);
      (this.props.isCreate
        ? k8sCreate(ko, this.state.data)
        : k8sPatch(ko, {metadata}, [{op: 'replace', path: `/subjects/${this.subjectIndex}`, value: subject}])
      ).then(obj => {
        this.setState({inProgress: false});
        if (metadata.namespace) {
          this.props.setActiveNamespace(metadata.namespace);
        }
        history.push(resourceObjPath(obj, referenceFor(obj)));
      }, err => this.setState({error: err.message, inProgress: false}));
    }

    render() {
      const {kind, metadata, roleRef} = this.state.data;
      const subject = this.getSubject();
      const {fixed, saveButtonText} = this.props;
      const RoleDropdown = kind === 'RoleBinding' ? NsRoleDropdown : ClusterRoleDropdown;
      const title = `${this.props.titleVerb} ${kindObj(kind).label}`;

      return <div className="co-m-pane__body">
        <Helmet>
          <title>{title}</title>
        </Helmet>
        <form className="co-m-pane__body-group co-m-pane__form" onSubmit={this.save}>
          <h1 className="co-m-pane__heading">{title}</h1>
          <p className="co-m-pane__explanation">Associate a user/group to the selected role to define the type of access and resources that are allowed.</p>

          {!_.get(fixed, 'kind') && <Section label="Binding Type">
            <RadioGroup currentValue={kind} items={bindingKinds} onChange={this.setKind} />
          </Section>}

          <div className="co-form-section__separator"></div>

          <Section label="Role Binding">
            <div className="form-group">
              <label htmlFor="role-binding-name" className="co-required">Name</label>
              {_.get(fixed, 'metadata.name')
                ? <ResourceName kind={kind} name={metadata.name} />
                : <input className="pf-c-form-control" type="text" onChange={this.changeName} placeholder="Role binding name" value={metadata.name} required id="role-binding-name" />}
            </div>
            {kind === 'RoleBinding' && <div className="form-group">
              <label htmlFor="ns-dropdown" className="co-required">Namespace</label>
              <NsDropdown fixed={!!_.get(fixed, 'metadata.namespace')} selectedKey={metadata.namespace} onChange={this.changeNamespace} id="ns-dropdown" />
            </div>}
          </Section>

          <div className="co-form-section__separator"></div>

          <Section label="Role">
            <div className="form-group">
              <label htmlFor="role-dropdown" className="co-required">Role Name</label>
              <RoleDropdown
                fixed={!!_.get(fixed, 'roleRef.name')}
                namespace={metadata.namespace}
                onChange={this.changeRoleRef}
                selectedKey={_.get(fixed, 'roleRef.name') || roleRef.name}
                selectedKeyKind={_.get(fixed, 'roleRef.kind') || roleRef.kind}
                id="role-dropdown"
              />
            </div>
          </Section>

          <div className="co-form-section__separator"></div>

          <Section label="Subject">
            <div className="form-group">
              <RadioGroup currentValue={subject.kind} items={subjectKinds} onChange={this.changeSubjectKind} />
            </div>
            {subject.kind === 'ServiceAccount' && <div className="form-group">
              <label htmlFor="subject-namespace" className="co-required">Subject Namespace</label>
              <NsDropdown id="subject-namespace" selectedKey={subject.namespace} onChange={this.changeSubjectNamespace} />
            </div>}
            <div className="form-group">
              <label htmlFor="subject-name" className="co-required">Subject Name</label>
              <input className="pf-c-form-control" type="text" onChange={this.changeSubjectName} placeholder="Subject name" value={subject.name} required id="subject-name" />
            </div>
          </Section>

          <div className="co-form-section__separator"></div>

          <ButtonBar errorMessage={this.state.error} inProgress={this.state.inProgress}>
            <ActionGroup className="pf-c-form">
              <Button
                type="submit"
                id="save-changes"
                variant="primary">
                {saveButtonText || 'Create'}
              </Button>
              <Button
                onClick={history.goBack}
                id="cancel"
                variant="secondary">
                Cancel
              </Button>
            </ActionGroup>
          </ButtonBar>
        </form>
      </div>;
    }
  });

export const CreateRoleBinding = ({match: {params}, location}) => {
  const searchParams = new URLSearchParams(location.search);
  const roleKind = searchParams.get('rolekind');
  const roleName = searchParams.get('rolename');
  const metadata = {namespace: UIActions.getActiveNamespace()};
  const clusterAllowed = useAccessReview({
    group: ClusterRoleBindingModel.apiGroup,
    resource: ClusterRoleBindingModel.plural,
    verb: 'create',
  });
  const fixed = {
    kind: (params.ns || roleKind === 'Role' || !clusterAllowed) ? 'RoleBinding' : undefined,
    metadata: {namespace: params.ns},
    roleRef: {kind: roleKind, name: roleName},
  };
  return <BaseEditRoleBinding
    metadata={metadata}
    fixed={fixed}
    isCreate={true}
    titleVerb="Create"
  />;
};

const getSubjectIndex = () => {
  const subjectIndex = getQueryArgument('subjectIndex') || '0';
  return parseInt(subjectIndex, 10);
};

const BindingLoadingWrapper = props => {
  const fixed = {};
  _.each(props.fixedKeys, k => fixed[k] = _.get(props.obj.data, k));
  return <StatusBox {...props.obj}>
    <BaseEditRoleBinding {...props} obj={props.obj.data} fixed={fixed} />
  </StatusBox>;
};

export const EditRoleBinding = ({match: {params}, kind}) => <Firehose resources={[{kind, name: params.name, namespace: params.ns, isList: false, prop: 'obj'}]}>
  <BindingLoadingWrapper fixedKeys={['kind', 'metadata', 'roleRef']} subjectIndex={getSubjectIndex()} titleVerb="Edit" saveButtonText="Save" />
</Firehose>;

export const CopyRoleBinding = ({match: {params}, kind}) => <Firehose resources={[{kind, name: params.name, namespace: params.ns, isList: false, prop: 'obj'}]}>
  <BindingLoadingWrapper isCreate={true} fixedKeys={['kind']} subjectIndex={getSubjectIndex()} titleVerb="Duplicate" />
</Firehose>;
