import type { TemplateResult } from "lit";
import { state, property } from "lit/decorators.js";
import { msg, localized, str } from "@lit/localize";
import { when } from "lit/directives/when.js";
import { guard } from "lit/directives/guard.js";
import { styleMap } from "lit/directives/style-map.js";
import { ref } from "lit/directives/ref.js";
import debounce from "lodash/fp/debounce";
import { mergeDeep } from "immutable";
import omit from "lodash/fp/omit";
import groupBy from "lodash/fp/groupBy";
import keyBy from "lodash/fp/keyBy";
import orderBy from "lodash/fp/orderBy";
import flow from "lodash/fp/flow";
import Fuse from "fuse.js";
import queryString from "query-string";
import { serialize } from "@shoelace-style/shoelace/dist/utilities/form.js";
import type { SlMenuItem } from "@shoelace-style/shoelace";

import type {
  CheckboxChangeEvent,
  CheckboxGroupList,
} from "../../components/checkbox-list";
import type { MarkdownChangeEvent } from "../../components/markdown-editor";
import type { AuthState } from "../../utils/AuthService";
import LiteElement, { html } from "../../utils/LiteElement";
import { maxLengthValidator } from "../../utils/form";
import type {
  APIPaginatedList,
  APIPaginationQuery,
  APISortQuery,
} from "../../types/api";
import type { Collection } from "../../types/collection";
import type { Crawl, CrawlState, Workflow } from "../../types/crawler";
import type { PageChangeEvent } from "../../components/pagination";

const TABS = ["crawls", "metadata"] as const;
type Tab = (typeof TABS)[number];
type SearchFields = "name" | "firstSeed";
type SearchResult = {
  item: {
    key: SearchFields;
    value: string;
  };
};
type SortField = "lastRun" | "modified" | "created" | "firstSeed";
type SortDirection = "asc" | "desc";
const sortableFields: Record<
  SortField,
  { label: string; defaultDirection?: SortDirection }
> = {
  lastRun: {
    label: msg("Latest Crawl"),
    defaultDirection: "desc",
  },
  modified: {
    label: msg("Last Modified"),
    defaultDirection: "desc",
  },
  created: {
    label: msg("Created At"),
    defaultDirection: "desc",
  },
  firstSeed: {
    label: msg("Crawl Start URL"),
    defaultDirection: "asc",
  },
};
const finishedCrawlStates: CrawlState[] = [
  "complete",
  "partial_complete",
  "timed_out",
];
const WORKFLOW_CRAWL_LIMIT = 100;
const WORKFLOW_PAGE_SIZE = 10;
const CRAWL_PAGE_SIZE = 5;
const MIN_SEARCH_LENGTH = 2;

export type CollectionSubmitEvent = CustomEvent<{
  values: {
    name: string;
    description: string | null;
    crawlIds: string[];
    oldCrawlIds?: string[];
  };
}>;

/**
 * @event on-submit
 */
@localized()
export class CollectionEditor extends LiteElement {
  @property({ type: Object })
  authState!: AuthState;

  @property({ type: String })
  orgId!: string;

  @property({ type: Boolean })
  isCrawler?: boolean;

  @property({ type: String })
  collectionId?: string;

  @property({ type: Object })
  metadataValues?: Collection;

  @property({ type: Boolean })
  isSubmitting = false;

  @state()
  private collectionCrawls?: Crawl[];

  // Store crawl IDs to compare later
  private savedCollectionCrawlIds: string[] = [];

  @state()
  private workflows?: APIPaginatedList & {
    items: Workflow[];
  };

  @state()
  private workflowPagination: {
    [workflowId: string]: APIPaginationQuery & {
      items: Workflow[];
    };
  } = {};

  @state()
  private workflowIsLoading: {
    [workflowId: string]: boolean;
  } = {};

  @state()
  private selectedCrawls: {
    [crawlId: string]: Crawl;
  } = {};

  @state()
  private activeTab: Tab = TABS[0];

  @state()
  private orderWorkflowsBy: {
    field: SortField;
    direction: SortDirection;
  } = {
    field: "lastRun",
    direction: sortableFields["lastRun"].defaultDirection!,
  };

  @state()
  private filterWorkflowsBy: Partial<Record<keyof Crawl, any>> = {};

  @state()
  private searchByValue: string = "";

  @state()
  private searchResultsOpen = false;

  private get hasSearchStr() {
    return this.searchByValue.length >= MIN_SEARCH_LENGTH;
  }

  private get selectedSearchFilterKey() {
    return Object.keys(this.fieldLabels).find((key) =>
      Boolean((this.filterWorkflowsBy as any)[key])
    );
  }

  // TODO localize
  private numberFormatter = new Intl.NumberFormat(undefined, {
    notation: "compact",
  });

  // For fuzzy search:
  private fuse = new Fuse([], {
    keys: ["value"],
    shouldSort: false,
    threshold: 0.2, // stricter; default is 0.6
  });

  private validateNameMax = maxLengthValidator(50);

  private readonly fieldLabels: Record<SearchFields, string> = {
    name: msg("Name"),
    firstSeed: msg("Crawl Start URL"),
  };

  private readonly tabLabels: Record<Tab, string> = {
    crawls: msg("Select Crawls"),
    metadata: msg("Metadata"),
  };

  protected async willUpdate(changedProperties: Map<string, any>) {
    if (changedProperties.has("orgId") && this.orgId) {
      this.fetchSearchValues();
    }
    if (
      (changedProperties.has("orgId") && this.orgId) ||
      changedProperties.has("filterWorkflowsBy") ||
      changedProperties.has("orderWorkflowsBy")
    ) {
      this.fetchWorkflows();
    }

    if (changedProperties.has("collectionId") && this.collectionId) {
      this.fetchCollectionCrawls();
    }
  }

  connectedCallback(): void {
    // Set initial active section and dialog based on URL #hash value
    this.getActivePanelFromHash();
    super.connectedCallback();
    window.addEventListener("hashchange", this.getActivePanelFromHash);
  }

  disconnectedCallback(): void {
    super.disconnectedCallback();
    window.removeEventListener("hashchange", this.getActivePanelFromHash);
  }

  render() {
    return html`<form
      name="collectionForm"
      autocomplete="off"
      @submit=${this.onSubmit}
    >
      <btrix-tab-list
        activePanel="collectionForm-${this.activeTab}"
        progressPanel="collectionForm-${this.activeTab}"
      >
        ${guard(
          [this.activeTab],
          () => html`
            <h3 slot="header" class="font-semibold">
              ${this.tabLabels[this.activeTab]}
            </h3>

            ${TABS.map(this.renderTab)}
          `
        )}

        <btrix-tab-panel name="collectionForm-crawls">
          ${this.renderSelectCrawls()}
        </btrix-tab-panel>
        <btrix-tab-panel name="collectionForm-metadata">
          ${guard(
            [this.metadataValues, this.isSubmitting, this.workflowIsLoading],
            this.renderMetadata
          )}
        </btrix-tab-panel>
      </btrix-tab-list>
    </form>`;
  }

  private renderTab = (tab: Tab) => {
    const isActive = tab === this.activeTab;
    const completed = false; // TODO
    const iconProps = {
      name: "circle",
      library: "default",
      class: "text-neutral-400",
    };
    if (isActive) {
      iconProps.name = "pencil-circle-dashed";
      iconProps.library = "app";
      iconProps.class = "text-base";
    } else if (completed) {
      iconProps.name = "check-circle";
    }

    return html`
      <btrix-tab
        slot="nav"
        name="collectionForm-${tab}"
        class="whitespace-nowrap"
        @click=${() => this.goToTab(tab)}
      >
        <sl-icon
          name=${iconProps.name}
          library=${iconProps.library}
          class="inline-block align-middle mr-1 text-base ${iconProps.class}"
        ></sl-icon>
        <span class="inline-block align-middle whitespace-normal">
          ${this.tabLabels[tab]}
        </span>
      </btrix-tab>
    `;
  };

  private renderSelectCrawls = () => {
    return html`
      <section class="grid grid-cols-1 md:grid-cols-2 gap-4">
        <section class="col-span-1 flex flex-col">
          <h4 class="text-base font-semibold mb-3">
            ${msg("Crawls in Collection")}
          </h4>
          <div class="border rounded-lg py-2 flex-1">
            ${guard(
              [
                this.isCrawler,
                this.collectionCrawls,
                this.selectedCrawls,
                this.workflowPagination,
              ],
              this.renderCollectionWorkflowList
            )}
          </div>
        </section>
        <section class="col-span-1 flex flex-col">
          <h4 class="text-base font-semibold mb-3">${msg("All Workflows")}</h4>
          ${when(
            this.workflows?.total,
            () =>
              html`
                <div class="flex-0 border rounded bg-neutral-50 p-2 mb-2">
                  ${guard(
                    [
                      this.searchResultsOpen,
                      this.searchByValue,
                      this.filterWorkflowsBy,
                      this.orderWorkflowsBy,
                    ],
                    this.renderWorkflowListControls
                  )}
                </div>
              `
          )}
          <div class="flex-1">
            ${guard(
              [
                this.isCrawler,
                this.workflows,
                this.collectionCrawls,
                this.selectedCrawls,
                this.workflowIsLoading,
              ],
              this.renderWorkflowList
            )}
          </div>
          <footer class="mt-4 flex justify-center">
            ${when(
              this.workflows?.total,
              () => html`
                <btrix-pagination
                  page=${this.workflows!.page}
                  totalCount=${this.workflows!.total}
                  size=${this.workflows!.pageSize}
                  @page-change=${async (e: PageChangeEvent) => {
                    await this.fetchWorkflows({
                      page: e.detail.page,
                    });

                    // Scroll to top of list
                    this.scrollIntoView({ behavior: "smooth" });
                  }}
                ></btrix-pagination>
              `
            )}
          </footer>
        </section>
        <footer
          class="col-span-full border rounded-lg px-6 py-4 flex justify-end"
        >
          ${when(
            this.collectionId,
            () => html`
              <sl-button
                size="small"
                variant="primary"
                ?disabled=${this.isSubmitting ||
                Object.values(this.workflowIsLoading).some(
                  (isLoading) => isLoading === true
                )}
                ?loading=${this.isSubmitting}
                @click=${this.submitCrawlSelectionChanges}
              >
                ${msg("Save Crawl Selection")}
              </sl-button>
            `,
            () => html`
              <sl-button size="small" @click=${() => this.goToTab("metadata")}>
                <sl-icon slot="suffix" name="chevron-right"></sl-icon>
                ${msg("Enter Metadata")}
              </sl-button>
            `
          )}
        </footer>
      </section>
    `;
  };

  private renderMetadata = () => {
    return html`
      <section class="border rounded-lg">
        <div class="p-6">
          <sl-input
            class="mb-2 with-max-help-text"
            name="name"
            label=${msg("Name")}
            placeholder=${msg("My Collection")}
            autocomplete="off"
            value=${this.metadataValues?.name}
            required
            help-text=${this.validateNameMax.helpText}
            @sl-input=${this.validateNameMax.validate}
          ></sl-input>

          <fieldset>
            <label class="form-label">${msg("Description")}</label>
            <btrix-markdown-editor
              name="description"
              initialValue=${this.metadataValues?.description}
              maxlength=${4000}
            ></btrix-markdown-editor>
          </fieldset>
        </div>
        <footer class="border-t px-6 py-4 flex justify-between">
          ${when(
            !this.collectionId,
            () => html`
              <sl-button size="small" @click=${() => this.goToTab("crawls")}>
                <sl-icon slot="prefix" name="chevron-left"></sl-icon>
                ${msg("Select Crawls")}
              </sl-button>
            `
          )}
          <sl-button
            class="ml-auto"
            type="submit"
            size="small"
            variant="primary"
            ?disabled=${this.isSubmitting ||
            Object.values(this.workflowIsLoading).some(
              (isLoading) => isLoading === true
            )}
            ?loading=${this.isSubmitting}
          >
            ${this.collectionId ? msg("Save Metadata") : msg("Save Collection")}
          </sl-button>
        </footer>
      </section>
    `;
  };

  private renderCollectionWorkflowList = () => {
    if (this.collectionId && !this.collectionCrawls) {
      return this.renderLoading();
    }

    if (!this.collectionCrawls?.length) {
      return html`
        <div
          class="flex flex-col items-center justify-center text-center p-4 my-12"
        >
          <span class="text-base font-semibold text-primary"
            >${msg("No Crawls in this Collection, yet")}</span
          >
          <p class="max-w-[24em] mx-auto mt-4">
            ${(this.workflows && !this.workflows.total) || !this.isCrawler
              ? msg(
                  "Select Workflows or individual Crawls. You can always come back and add Crawls later."
                )
              : msg(
                  "Create a Workflow to select Crawls. You can always come back and add Crawls later."
                )}
          </p>
        </div>
      `;
    }
    const groupedByWorkflow = groupBy("cid")(this.collectionCrawls) as any;

    return html`
      <btrix-checkbox-list>
        ${Object.keys(groupedByWorkflow).map((workflowId) =>
          this.renderWorkflowCrawls(
            workflowId,
            orderBy(["finished"])(["desc"])(
              groupedByWorkflow[workflowId]
            ) as any
          )
        )}
      </btrix-checkbox-list>
    `;
  };

  private renderWorkflowCrawls(workflowId: string, crawls: Crawl[]) {
    const selectedCrawlIds = crawls
      .filter(({ id }) => this.selectedCrawls[id])
      .map(({ id }) => id);
    const allChecked = crawls.length === selectedCrawlIds.length;
    // Use latest crawl for workflow information, since we
    // may not have access to workflow details
    const firstCrawl = crawls[0];

    return html`
      <btrix-checkbox-list-item
        ?checked=${selectedCrawlIds.length}
        ?allChecked=${allChecked}
        group
        aria-controls=${selectedCrawlIds.join(" ")}
        @on-change=${(e: CheckboxChangeEvent) => {
          if (e.detail.checked || !allChecked) {
            this.selectCrawls(crawls);
          } else {
            this.deselectCrawls(crawls);
          }
        }}
      >
        <div class="grid grid-cols-[1fr_4.6rem_2.5rem] gap-3 items-center">
          <div class="col-span-1 min-w-0 truncate">
            ${this.renderCrawlName(firstCrawl)}
          </div>
          <div
            class="col-span-1 text-neutral-500 text-xs font-monostyle truncate h-4"
          >
            ${crawls.length === 1
              ? msg("1 crawl")
              : msg(str`${this.numberFormatter.format(crawls.length)} crawls`)}
          </div>
          <div class="col-span-1 border-l flex items-center justify-center">
            <btrix-button
              class="expandBtn p-2 text-base transition-transform"
              aria-label=${msg("Expand row")}
              aria-expanded="false"
              aria-controls=${`workflow-${workflowId}`}
              @click=${(e: MouseEvent) => {
                e.preventDefault();
                e.stopPropagation();
                this.toggleWorkflow(workflowId);
              }}
              icon
            >
              <sl-icon name="chevron-double-down"></sl-icon>
            </btrix-button>
          </div>
        </div>
        <section
          id=${`workflow-${workflowId}-group`}
          slot="group"
          class="checkboxGroup overflow-hidden offscreen"
          ${ref(this.checkboxGroupUpdated)}
        >
          ${guard(
            [this.selectedCrawls, this.workflowPagination[workflowId]],
            () => this.renderWorkflowCrawlList(workflowId, crawls)
          )}
        </section>
      </btrix-checkbox-list-item>
    `;
  }

  private renderWorkflowCrawlList = (workflowId: string, crawls: Crawl[]) => {
    const { page = 1 } = this.workflowPagination[workflowId] || {};
    return html`
      <btrix-checkbox-group-list>
        ${crawls
          .slice((page - 1) * CRAWL_PAGE_SIZE, page * CRAWL_PAGE_SIZE)
          .map((crawl) => this.renderCrawl(crawl, workflowId))}
      </btrix-checkbox-group-list>

      ${when(
        crawls.length > CRAWL_PAGE_SIZE,
        () => html`
          <footer class="flex justify-center">
            <btrix-pagination
              page=${page}
              totalCount=${crawls.length}
              size=${CRAWL_PAGE_SIZE}
              compact
              @page-change=${async (e: PageChangeEvent) => {
                this.workflowPagination = mergeDeep(this.workflowPagination, {
                  [workflowId]: { page: e.detail.page },
                });
              }}
            ></btrix-pagination>
          </footer>
        `
      )}
    `;
  };

  private renderCrawl(crawl: Crawl, workflowId?: string) {
    return html`
      <btrix-checkbox-list-item
        id=${crawl.id}
        name="crawlIds"
        value=${crawl.id}
        ?checked=${this.selectedCrawls[crawl.id]}
        @on-change=${(e: CheckboxChangeEvent) => {
          if (e.detail.checked) {
            this.selectedCrawls = mergeDeep(this.selectedCrawls, {
              [crawl.id]: crawl,
            });
          } else {
            this.selectedCrawls = omit([crawl.id])(this.selectedCrawls) as any;
          }
        }}
      >
        <div class="flex items-center">
          <btrix-crawl-status
            state=${crawl.state}
            hideLabel
          ></btrix-crawl-status>
          <div class="flex-1">
            ${workflowId
              ? html`<sl-format-date
                  date=${`${crawl.finished}Z`}
                  month="2-digit"
                  day="2-digit"
                  year="2-digit"
                  hour="2-digit"
                  minute="2-digit"
                ></sl-format-date>`
              : this.renderSeedsLabel(crawl.firstSeed, crawl.seedCount)}
          </div>
          <div class="w-16 font-monostyle truncate">
            <sl-tooltip content=${msg("Pages in crawl")}>
              <div class="flex items-center">
                <sl-icon
                  class="text-base"
                  name="file-earmark-richtext"
                ></sl-icon>
                <div class="ml-1 text-xs">
                  ${this.numberFormatter.format(+(crawl.stats?.done || 0))}
                </div>
              </div>
            </sl-tooltip>
          </div>
          <div class="w-14">
            <sl-format-bytes
              class="text-neutral-500 text-xs font-monostyle"
              value=${crawl.fileSize || 0}
              display="narrow"
            ></sl-format-bytes>
          </div>
        </div>
      </btrix-checkbox-list-item>
    `;
  }

  private renderWorkflowListControls = () => {
    return html`
      <div>
        <div class="mb-2">${this.renderSearch()}</div>
        <div class="flex items-center">
          <div class="whitespace-nowrap text-neutral-500 mx-2">
            ${msg("Sort by:")}
          </div>
          <sl-select
            class="flex-1"
            size="small"
            pill
            value=${this.orderWorkflowsBy.field}
            @sl-change=${(e: Event) => {
              const field = (e.target as HTMLSelectElement).value as SortField;
              this.orderWorkflowsBy = {
                field: field,
                direction:
                  sortableFields[field].defaultDirection ||
                  this.orderWorkflowsBy.direction,
              };
            }}
          >
            ${Object.entries(sortableFields).map(
              ([value, { label }]) => html`
                <sl-option value=${value}>${label}</sl-option>
              `
            )}
          </sl-select>
          <sl-icon-button
            name="arrow-down-up"
            label=${msg("Reverse sort")}
            @click=${() => {
              this.orderWorkflowsBy = {
                ...this.orderWorkflowsBy,
                direction:
                  this.orderWorkflowsBy.direction === "asc" ? "desc" : "asc",
              };
            }}
          ></sl-icon-button>
        </div>
      </div>
    `;
  };

  private renderSearch() {
    return html`
      <btrix-combobox
        ?open=${this.searchResultsOpen}
        @request-close=${() => {
          this.searchResultsOpen = false;
          this.searchByValue = "";
        }}
        @sl-select=${async (e: CustomEvent) => {
          this.searchResultsOpen = false;
          const item = e.detail.item as SlMenuItem;
          const key = item.dataset["key"] as SearchFields;
          this.searchByValue = item.value;
          await this.updateComplete;
          this.filterWorkflowsBy = {
            ...this.filterWorkflowsBy,
            [key]: item.value,
          };
        }}
      >
        <sl-input
          size="small"
          placeholder=${msg("Search by name or Crawl Start URL")}
          clearable
          value=${this.searchByValue}
          @sl-clear=${() => {
            this.searchResultsOpen = false;
            this.onSearchInput.cancel();
            const { name, firstSeed, ...otherFilters } = this.filterWorkflowsBy;
            this.filterWorkflowsBy = otherFilters;
          }}
          @sl-input=${this.onSearchInput}
        >
          ${when(
            this.selectedSearchFilterKey,
            () =>
              html`<sl-tag
                slot="prefix"
                size="small"
                pill
                style="margin-left: var(--sl-spacing-3x-small)"
                >${this.fieldLabels[
                  this.selectedSearchFilterKey as SearchFields
                ]}</sl-tag
              >`,
            () => html`<sl-icon name="search" slot="prefix"></sl-icon>`
          )}
        </sl-input>
        ${this.renderSearchResults()}
      </btrix-combobox>
    `;
  }

  private renderSearchResults() {
    if (!this.hasSearchStr) {
      return html`
        <sl-menu-item slot="menu-item" disabled
          >${msg("Start typing to view crawl filters.")}</sl-menu-item
        >
      `;
    }

    const searchResults = this.fuse.search(this.searchByValue).slice(0, 10);
    if (!searchResults.length) {
      return html`
        <sl-menu-item slot="menu-item" disabled
          >${msg("No matching crawls found.")}</sl-menu-item
        >
      `;
    }

    return html`
      ${searchResults.map(
        ({ item }: SearchResult) => html`
          <sl-menu-item
            slot="menu-item"
            data-key=${item.key}
            value=${item.value}
          >
            <sl-tag slot="prefix" size="small" pill
              >${this.fieldLabels[item.key]}</sl-tag
            >
            ${item.value}
          </sl-menu-item>
        `
      )}
    `;
  }

  private renderWorkflowList = () => {
    if (!this.workflows) {
      return this.renderLoading();
    }

    if (!this.workflows.total) {
      return html`
        <div class="h-full flex justify-center items-center">
          ${when(
            this.isCrawler,
            () => html`
              <sl-button
                href=${`/orgs/${this.orgId}/workflows?new&jobType=`}
                variant="primary"
                @click=${this.navLink}
              >
                <sl-icon slot="prefix" name="plus-lg"></sl-icon>
                ${msg("New Crawl Workflow")}
              </sl-button>
            `,
            () =>
              html`
                <p class="text-neutral-400 text-center max-w-[24em]">
                  ${msg("Your organization doesn't have any Crawl Workflows.")}
                </p>
              `
          )}
        </div>
      `;
    }

    const groupedByWorkflow = groupBy("cid")(this.collectionCrawls) as any;

    return html`
      <btrix-checkbox-list>
        ${this.workflows.items.map((workflow) =>
          this.renderWorkflowItem(workflow, groupedByWorkflow[workflow.id])
        )}
      </btrix-checkbox-list>
    `;
  };

  private renderWorkflowItem(workflow: Workflow, crawls: Crawl[] = []) {
    const selectedCrawls = crawls.filter(({ id }) => this.selectedCrawls[id]);
    const allChecked = workflow.crawlSuccessfulCount === selectedCrawls.length;
    return html`
      <btrix-checkbox-list-item
        ?checked=${selectedCrawls.length}
        ?allChecked=${allChecked}
        ?disabled=${this.collectionId && !this.collectionCrawls}
        group
        @on-change=${(e: CheckboxChangeEvent) => {
          if (e.detail.checked || !allChecked) {
            this.selectWorkflow(workflow.id);
          } else {
            this.deselectCrawls(crawls);
          }
        }}
      >
        <div class="relative">
          <div class="grid grid-cols-[1fr_12ch] gap-3">
            ${this.renderWorkflowDetails(workflow)}
          </div>
          ${this.workflowIsLoading[workflow.id]
            ? html`<div
                class="absolute top-0 left-0 right-0 bottom-0 bg-white bg-opacity-50 flex items-center justify-center text-lg -ml-11"
              >
                <sl-spinner></sl-spinner>
              </div>`
            : ""}
        </div>
      </btrix-checkbox-list-item>
    `;
  }

  private renderWorkflowDetails(workflow: Workflow) {
    return html`
      <div class="col-span-1 py-3 whitespace-nowrap truncate">
        <div class="text-neutral-700 h-6 truncate">
          ${this.renderCrawlName(workflow)}
        </div>
        <div class="text-neutral-500 text-xs font-monostyle truncate h-4">
          <sl-format-date
            date=${workflow.lastCrawlTime || workflow.modified}
            month="2-digit"
            day="2-digit"
            year="2-digit"
            hour="2-digit"
            minute="2-digit"
          ></sl-format-date>
        </div>
      </div>
      <div class="col-span-1 py-3">
        <div class="text-neutral-700 truncate h-6">
          <sl-format-bytes
            value=${workflow.totalSize}
            display="narrow"
          ></sl-format-bytes>
        </div>
        <div class="text-neutral-500 text-xs font-monostyle truncate h-4">
          ${this.renderCrawlCount(workflow)}
        </div>
      </div>
    `;
  }

  private renderCrawlCount(workflow: Workflow) {
    const count = Math.min(WORKFLOW_CRAWL_LIMIT, workflow.crawlSuccessfulCount);
    let message = "";
    if (count === 1) {
      message = msg("1 crawl");
    } else {
      message = msg(str`${this.numberFormatter.format(count)} crawls`);
    }
    return html`<span class="inline-block align-middle">${message}</span
      >${workflow.crawlSuccessfulCount > count
        ? html`<sl-tooltip
            content=${msg(
              str`Only showing latest ${WORKFLOW_CRAWL_LIMIT} crawls`
            )}
          >
            <sl-icon
              class="inline-block align-middle"
              name="exclamation-triangle"
            ></sl-icon>
          </sl-tooltip>`
        : ""}`;
  }

  private renderCrawlName(item: Workflow | Crawl) {
    if (item.name) return html`<span class="min-w-0">${item.name}</span>`;
    if (!item.firstSeed) return html`<span class="min-w-0">${item.id}</span>`;
    return this.renderSeedsLabel(
      item.firstSeed,
      (item as Crawl).seedCount || item.config?.seeds.length
    );
  }

  private renderSeedsLabel(firstSeed: string, seedCount: number) {
    let nameSuffix: any = "";
    const remainder = seedCount - 1;
    if (remainder) {
      if (remainder === 1) {
        nameSuffix = html`<span class="ml-1 text-neutral-500"
          >${msg(str`+${this.numberFormatter.format(remainder)} URL`)}</span
        >`;
      } else {
        nameSuffix = html`<span class="ml-1 text-neutral-500"
          >${msg(str`+${this.numberFormatter.format(remainder)} URLs`)}</span
        >`;
      }
    }
    return html`
      <div class="flex">
        <span class="min-w-0 truncate">${firstSeed}</span>${nameSuffix}
      </div>
    `;
  }

  private renderLoading = () => html`
    <div class="w-full flex items-center justify-center my-24 text-3xl">
      <sl-spinner></sl-spinner>
    </div>
  `;

  private selectCrawls(crawls: Crawl[]) {
    const allCrawls = crawls.reduce(
      (acc: any, crawl: Crawl) => ({
        ...acc,
        [crawl.id]: crawl,
      }),
      {}
    );
    this.selectedCrawls = mergeDeep(this.selectedCrawls, allCrawls);
  }

  private deselectCrawls(crawls: Crawl[]) {
    this.selectedCrawls = omit(crawls.map(({ id }) => id))(
      this.selectedCrawls
    ) as any;
  }

  private async selectWorkflow(workflowId: string) {
    const crawls = await this.fetchWorkflowCrawls(workflowId);
    this.selectCrawls(crawls);
  }

  private checkboxGroupUpdated = async (el: any) => {
    await this.updateComplete;
    if (el) {
      await el.updateComplete;
      if (el.classList.contains("offscreen")) {
        // Set up initial position for expand/contract toggle
        el.style.marginTop = `-${el.clientHeight}px`;
        el.style.opacity = "0";
        el.style.pointerEvents = "none";
        el.classList.remove("offscreen");
      }
    }
  };

  private toggleWorkflow = async (workflowId: string) => {
    const checkboxGroup = this.querySelector(
      `#workflow-${workflowId}-group`
    ) as HTMLElement;
    const listItem = checkboxGroup.closest(
      "btrix-checkbox-list-item"
    ) as HTMLElement;
    const expandBtn = listItem.querySelector(".expandBtn") as HTMLElement;
    const expand = !(expandBtn.getAttribute("aria-expanded") === "true");
    expandBtn.setAttribute("aria-expanded", expand.toString());
    checkboxGroup.classList.add("transition-all");

    if (expand) {
      expandBtn.classList.add("rotate-180");
      checkboxGroup.style.marginTop = "0px";
      checkboxGroup.style.opacity = "100%";
      checkboxGroup.style.pointerEvents = "auto";
    } else {
      expandBtn.classList.remove("rotate-180");
      checkboxGroup.style.marginTop = `-${checkboxGroup.clientHeight}px`;
      checkboxGroup.style.opacity = "0";
      checkboxGroup.style.pointerEvents = "none";
    }
  };

  private onSearchInput = debounce(150)((e: any) => {
    this.searchByValue = e.target.value.trim();

    if (this.searchResultsOpen === false && this.hasSearchStr) {
      this.searchResultsOpen = true;
    }

    if (!this.searchByValue && this.selectedSearchFilterKey) {
      const {
        [this.selectedSearchFilterKey as SearchFields]: _,
        ...otherFilters
      } = this.filterWorkflowsBy;
      this.filterWorkflowsBy = {
        ...otherFilters,
      };
    }
  }) as any;

  private async submitCrawlSelectionChanges() {
    this.dispatchEvent(
      <CollectionSubmitEvent>new CustomEvent("on-submit", {
        detail: {
          values: {
            oldCrawlIds: this.savedCollectionCrawlIds,
            crawlIds: Object.keys(this.selectedCrawls),
          },
        },
      })
    );
  }

  private async onSubmit(event: SubmitEvent) {
    event.preventDefault();
    event.stopPropagation();
    await this.updateComplete;

    const form = event.target as HTMLFormElement;
    if (form.querySelector("[data-invalid]")) {
      return;
    }

    const values = serialize(form);
    if (!this.collectionId) {
      // Crawl IDs can only be saved in new collections
      values.crawlIds = Object.keys(this.selectedCrawls);
    }
    this.dispatchEvent(
      <CollectionSubmitEvent>new CustomEvent("on-submit", {
        detail: { values },
      })
    );
  }

  private getActivePanelFromHash = () => {
    const hashValue = window.location.hash.slice(1);
    if (TABS.includes(hashValue as any)) {
      this.activeTab = hashValue as Tab;
    } else {
      this.goToTab(TABS[0], { replace: true });
    }
  };

  private goToTab(tab: Tab, { replace = false } = {}) {
    const path = `${window.location.href.split("#")[0]}#${tab}`;
    if (replace) {
      window.history.replaceState(null, "", path);
    } else {
      window.history.pushState(null, "", path);
    }
    this.activeTab = tab;
  }

  private async fetchWorkflows(params: APIPaginationQuery = {}) {
    try {
      this.workflows = await this.getWorkflows({
        page: params.page || this.workflows?.page || 1,
        pageSize:
          params.pageSize || this.workflows?.pageSize || WORKFLOW_PAGE_SIZE,
      });
    } catch (e: any) {
      this.notify({
        message: msg("Sorry, couldn't retrieve Workflows at this time."),
        variant: "danger",
        icon: "exclamation-octagon",
      });
    }
  }

  private async getWorkflows(
    params: APIPaginationQuery
  ): Promise<APIPaginatedList> {
    const query = queryString.stringify({
      ...params,
      ...this.filterWorkflowsBy,
      sortBy: this.orderWorkflowsBy.field,
      sortDirection: this.orderWorkflowsBy.direction === "desc" ? -1 : 1,
    });
    const data: APIPaginatedList = await this.apiFetch(
      `/orgs/${this.orgId}/crawlconfigs?${query}`,
      this.authState!
    );

    return data;
  }

  private async fetchCollectionCrawls() {
    if (!this.collectionId) return;

    try {
      const { items: crawls } = await this.getCrawls({
        collectionId: this.collectionId,
        sortBy: "finished",
        pageSize: WORKFLOW_CRAWL_LIMIT,
      });
      this.selectedCrawls = mergeDeep(
        this.selectedCrawls,
        crawls.reduce(
          (acc, crawl) => ({
            ...acc,
            [crawl.id]: crawl,
          }),
          {}
        )
      );
      // TODO remove omit once API removes errors
      this.collectionCrawls = crawls.map(omit("errors")) as Crawl[];
      // Store crawl IDs to compare later
      this.savedCollectionCrawlIds = this.collectionCrawls.map(({ id }) => id);
    } catch {
      this.notify({
        message: msg(
          "Sorry, couldn't retrieve Crawls in Collection at this time."
        ),
        variant: "danger",
        icon: "exclamation-octagon",
      });
    }
  }

  private async fetchWorkflowCrawls(workflowId: string): Promise<Crawl[]> {
    this.workflowIsLoading = mergeDeep(this.workflowIsLoading, {
      [workflowId]: true,
    });

    let workflowCrawls: Crawl[] = [];

    try {
      const { items } = await this.getCrawls({
        cid: workflowId,
        state: finishedCrawlStates,
        sortBy: "finished",
        pageSize: WORKFLOW_CRAWL_LIMIT,
      });
      // TODO remove omit once API removes errors
      const crawls = items.map(omit("errors")) as Crawl[];
      this.collectionCrawls = flow(
        keyBy("id"),
        (res) => mergeDeep(keyBy("id")(this.collectionCrawls), res),
        Object.values
      )(crawls) as any;

      workflowCrawls = crawls;
    } catch {
      this.notify({
        message: msg("Sorry, couldn't retrieve Crawl Workflow at this time."),
        variant: "danger",
        icon: "exclamation-octagon",
      });
    }

    this.workflowIsLoading = mergeDeep(this.workflowIsLoading, {
      [workflowId]: false,
    });

    return workflowCrawls;
  }

  private async getCrawls(
    params: Partial<{
      cid?: string;
      collectionId?: string;
      state: CrawlState[];
    }> &
      APIPaginationQuery &
      APISortQuery
  ): Promise<APIPaginatedList> {
    const query = queryString.stringify(params || {}, {
      arrayFormat: "comma",
    });
    const data: APIPaginatedList = await this.apiFetch(
      `/orgs/${this.orgId}/crawls?${query}`,
      this.authState!
    );

    return data;
  }

  private async fetchSearchValues() {
    try {
      const { names, firstSeeds } = await this.apiFetch(
        `/orgs/${this.orgId}/crawlconfigs/search-values`,
        this.authState!
      );

      // Update search/filter collection
      const toSearchItem =
        (key: SearchFields) =>
        (value: string): SearchResult["item"] => ({
          key,
          value,
        });
      this.fuse.setCollection([
        ...names.map(toSearchItem("name")),
        ...firstSeeds.map(toSearchItem("firstSeed")),
      ] as any);
    } catch (e) {
      console.debug(e);
    }
  }
}
customElements.define("btrix-collection-editor", CollectionEditor);
