import { __decorate } from "tslib";
import { ErrorResponse, } from "@api-client/core/build/browser.js";
import { html, nothing } from "lit";
import { property, state } from "lit/decorators.js";
import ApiElement from "../ApiElement.js";
import { relativeDay } from "../../lib/time/Conversion.js";
import { HttpHistoryChart } from '../../lib/chart/HttpHistoryChart.js';
import { statusTemplate, } from '../http/HttpStatus.js';
import { Events } from '../../events/Events.js';
import { StoreBroadcast } from "../../http-client/store/StoreBroadcast.js";
import '@github/relative-time-element';
import { SnackNotifications } from "../../ui/notification/SnackNotifications.js";
import '../../define/http/http-request-log.js';
import '../../define/ui/ui-icon.js';
import '../../define/ui/ui-button.js';
import '../../define/ui/ui-dropdown-list.js';
import '../../define/ui/ui-list.js';
import '../../define/ui/ui-list-item.js';
import '../../define/ui/ui-divider.js';
import '../../define/ui/ui-segmented-button.js';
import '../../define/ui/ui-segmented-button-set.js';
import '../../define/ui/ui-dialog.js';
export var ProjectHistoryView;
(function (ProjectHistoryView) {
    ProjectHistoryView["history"] = "history";
    ProjectHistoryView["analysis"] = "analysis";
})(ProjectHistoryView || (ProjectHistoryView = {}));
/**
 * An element that renders HTTP history for a configured list of requests.
 * This element is just a view and contains no logic related to store communication
 * or user interactions. Use the dispatched events to handle user interactions.
 */
export default class HttpProjectRequestHistory extends ApiElement {
    /**
     * Returns true when the element is correctly setup.
     */
    get isSetup() {
        return !!this.project && !!this.request;
    }
    constructor() {
        super();
        /**
         * The history list to render.
         */
        this.history = [];
        /**
         * Whether querying for a page of results.
         */
        this.querying = false;
        this.view = ProjectHistoryView.history;
        this.channel = new BroadcastChannel(StoreBroadcast.history);
        this.handleBroadcast = this.handleBroadcast.bind(this);
    }
    willUpdate(cp) {
        if (cp.has('project') || cp.has('request')) {
            this.reset();
            this.queryDebounce();
        }
        if (cp.has('selectedItem')) {
            this.computeSelectedItem();
        }
        super.willUpdate(cp);
    }
    connectedCallback() {
        super.connectedCallback();
        this.channel.addEventListener('message', this.handleBroadcast);
    }
    disconnectedCallback() {
        super.disconnectedCallback();
        this.channel.removeEventListener('message', this.handleBroadcast);
    }
    handleBroadcast(e) {
        const event = e.data;
        switch (event.operation) {
            case 'created':
                this.handleHistoryCreated(event);
                break;
            case 'deleted':
                this.handleHistoryDeleted(event);
                break;
            default:
        }
    }
    handleHistoryCreated(event) {
        const { request, data } = event;
        const item = data;
        if (this.request !== request) {
            return;
        }
        if (item) {
            this.addItem(item, true);
        }
    }
    handleHistoryDeleted(event) {
        const { model, history } = this;
        const { key, request } = event;
        if (!model || !history || request !== this.request) {
            return;
        }
        const historyIndex = history.findIndex(i => i.key === key);
        if (historyIndex < 0) {
            return;
        }
        history.splice(historyIndex, 1);
        for (const group of model) {
            const index = group.findIndex(i => i.key === key);
            if (index >= 0) {
                group.splice(index, 1);
                if (group.length === 0) {
                    const groupIndex = model.indexOf(group);
                    model.splice(groupIndex, 1);
                }
                this.requestUpdate();
                this.setupVisibility();
                break;
            }
        }
    }
    setupVisibility() {
        if (this.history && !this.history.length) {
            this.setAttribute('hidden', '');
        }
        else {
            this.removeAttribute('hidden');
        }
    }
    reset() {
        this.history = [];
        this.pageCursor = undefined;
        this.model = undefined;
        this.selectedItem = undefined;
        this.setupVisibility();
        if (this.debounceTimeout) {
            clearTimeout(this.debounceTimeout);
            this.debounceTimeout = undefined;
        }
    }
    queryDebounce() {
        if (!this.isSetup) {
            return;
        }
        if (this.debounceTimeout) {
            clearTimeout(this.debounceTimeout);
        }
        this.debounceTimeout = setTimeout(() => {
            delete this.debounceTimeout;
            this.queryPage();
        }, 1);
    }
    async queryPage() {
        const { project, request, pageCursor } = this;
        if (!project || !request || this.querying) {
            // todo: set `dirty` flag and re-query after finish.
            return;
        }
        this.querying = true;
        const init = {
            type: 'request',
            project,
            id: request,
            space: '', // this is filled by the bindings
        };
        if (pageCursor) {
            init.cursor = pageCursor;
        }
        try {
            const result = await Events.Store.History.list(init, this);
            if (!result) {
                return;
            }
            const { items, cursor } = result;
            if (items) {
                this.addItems(items);
            }
            if (cursor) {
                this.pageCursor = cursor;
            }
        }
        catch (e) {
            // ...
        }
        finally {
            this.querying = false;
            this.setupVisibility();
        }
    }
    addItems(items) {
        const { history = [] } = this;
        this.history = history.concat(items);
        const all = history.concat(items);
        const sorted = this.sortHistory(all);
        this.model = sorted;
        this.computeSelectedItem();
        if (sorted.length && !this.renderedItem) {
            this.selectedItem = sorted[0][0].key;
        }
        this.setupVisibility();
    }
    /**
     * Adds a new history to the list.
     * This is used instead of re-assigning the entire array and triggering initial computations.
     * @param item The history item to add
     * @param select When true it makes this item selected,
     */
    addItem(item, select) {
        const { model } = this;
        this.history.push(item);
        if (!model || !model.length) {
            this.model = [[item]];
            this.selectedItem = item.key;
            this.requestUpdate();
            this.setupVisibility();
            return;
        }
        const { midnight = Date.now() } = item;
        const index = model.findIndex(i => i[0].midnight === midnight);
        if (index >= 0) {
            this.pushSorted(model[index], item);
        }
        else {
            const position = model.findIndex(i => (i[0] && i[0].midnight || 0) < midnight);
            model.splice(position, 0, [item]);
        }
        if (select && item.key) {
            this.selectedItem = item.key;
        }
        this.requestUpdate();
        this.setupVisibility();
    }
    sortHistory(history) {
        if (!Array.isArray(history) || !history.length) {
            return [];
        }
        const result = [];
        const now = Date.now();
        history.forEach((current) => {
            const { midnight = now } = current;
            const index = result.findIndex(i => i[0].midnight === midnight);
            if (index >= 0) {
                this.pushSorted(result[index], current);
            }
            else if (result[0] && result[0][0] && result[0][0].midnight && result[0][0].midnight < midnight) {
                result.unshift([current]);
            }
            else {
                result.push([current]);
            }
        });
        return result;
    }
    computeSelectedItem() {
        const { history, selectedItem } = this;
        this.renderedItem = undefined;
        if (!selectedItem || !Array.isArray(history)) {
            return;
        }
        this.renderedItem = history.find(i => i.key === selectedItem);
    }
    /**
     * Allows to push a history item into a right index depending on the `created` property
     * so the data processor can skip sorting each group after mutation.
     *
     * The item is inserted before an item that is older (has lower `created` property).
     *
     * @param list The list to insert the item to
     * @param item The item to insert.
     */
    pushSorted(list, item) {
        const older = list.findIndex(i => i.created < item.created);
        if (older >= 0) {
            list.splice(older, 0, item);
        }
        else {
            // this item is the oldest
            list.push(item);
        }
    }
    handleSelection(e) {
        const item = e.detail.item;
        if (!item) {
            return;
        }
        const { key } = item.dataset;
        if (!key) {
            return;
        }
        this.selectedItem = key;
    }
    handleViewSelection(e) {
        if (!e.detail.selected) {
            return;
        }
        const button = e.detail.button;
        const view = button.dataset.value;
        if (view) {
            this.view = view;
        }
    }
    handleHistoryOption(e) {
        const element = e.detail.item;
        const { option } = element.dataset;
        if (!option) {
            return;
        }
        if (option === 'clear') {
            this.triggerClearHistoryDialog();
        }
    }
    triggerClearHistoryDialog() {
        const dialog = this.shadowRoot?.querySelector('.clear-dialog');
        if (!dialog) {
            return;
        }
        dialog.open = true;
    }
    handleClearHistoryDialogResult(e) {
        if (e.detail.cancelled) {
            return;
        }
        this.clearHistory();
    }
    async clearHistory() {
        const { project, request } = this;
        if (!project || !request) {
            return;
        }
        // space parameter is filled by bindings.
        await Events.Store.History.clearRequest('', project, request, this);
        SnackNotifications.notify('History cleared');
        this.reset();
    }
    handleItemDelete(e) {
        const button = e.currentTarget;
        const { key } = button.dataset;
        if (!key) {
            return;
        }
        this.deleteHistoryItem(key);
    }
    async deleteHistoryItem(key) {
        if (!key) {
            return;
        }
        const item = this.history.find(i => i.key === key);
        if (!item) {
            return;
        }
        await Events.Store.History.delete(key, this.request, this);
        SnackNotifications.notify('History item deleted', {
            actionLabel: 'Undo',
            actionCallback: () => {
                this.revertHistoryItemDelete(item);
            },
            timeout: 7000,
        });
    }
    async revertHistoryItemDelete(item) {
        const info = SnackNotifications.notify('Reverting...', { persistent: true });
        try {
            await Events.Store.History.create(item, this);
            SnackNotifications.cancel(info);
            SnackNotifications.notify('Reverted history item.');
        }
        catch (e) {
            SnackNotifications.cancel(info);
            SnackNotifications.notify('Unable to revert changes.');
        }
    }
    render() {
        const { model } = this;
        if (!model || !model.length) {
            if (this.querying) {
                return this.renderLoader();
            }
            return nothing;
        }
        return html `
      ${this.renderHeader()}
      ${this.renderView()}
      ${this.renderClearHistoryDialog()}
    `;
    }
    renderLoader() {
        return html `<progress></progress>`;
    }
    renderHeader() {
        return html `
    <div class="header">
      ${this.renderHistoryDropdown()}
      ${this.renderHistoryOptions()}
      ${this.renderViewToggle()}
    </div>
    `;
    }
    renderHistoryDropdown() {
        const { model } = this;
        const disabled = !model || !model.length;
        return html `
    <ui-dropdown-list
      verticalAlign="top"
      class="history-dropdown"
      closeOnOutsideClick
      @select="${this.handleSelection}"
    >
      <ui-button type="tonal" ?disabled="${disabled}">
        <ui-icon slot="icon" icon="arrowDropDown"></ui-icon>
        History
      </ui-button>
      <ui-list slot="dropdown" role="menu" aria-label="History list">
        ${this.renderHistoryList()}
      </ui-list>
    </ui-dropdown-list>
    `;
    }
    renderHistoryOptions() {
        return html `
    <ui-dropdown-list @select="${this.handleHistoryOption}" closeOnOutsideClick class="dropdown">
      <ui-icon-button aria-label="Additional options"><ui-icon icon="moreVert" role="presentation"></ui-icon></ui-icon-button>
      <ui-list slot="dropdown" role="menu">
        <ui-list-item role="menuitem" data-option="clear" image="icon" title="Deletes all history for the request">
          <ui-icon role="presentation" slot="start" icon="deleteOutline"></ui-icon>
          Clear history
        </ui-list-item>
      </ui-list>
    </ui-dropdown-list>
    `;
    }
    renderHistoryList() {
        const { model } = this;
        if (!Array.isArray(model) || !model.length) {
            return nothing;
        }
        return model.map(group => this.renderHistoryGroup(group));
    }
    renderHistoryGroup(group) {
        if (!group.length) {
            // this should not happen as the model is computed internally, 
            // but I prefer to do just one check to not stop execution when error.
            return nothing;
        }
        const [first] = group;
        return html `
    ${this.renderHistoryGroupTitle(first)}
    ${group.map(item => this.renderHistoryItem(item))}
    `;
    }
    renderHistoryGroupTitle(item) {
        const { midnight = 0 } = item;
        return html `
    <div class="history-group-title">${relativeDay(midnight)}</div>
    <ui-divider type="inset"></ui-divider>
    `;
    }
    renderHistoryItem(item) {
        const d = new Date(item.created);
        const isoTime = d.toISOString();
        const { log } = item;
        const { request, response } = log;
        const method = request && request.method;
        const url = request && request.url;
        if (!method || !url) {
            return nothing;
        }
        const error = !response || ErrorResponse.isErrorResponse(response);
        let status = 0;
        let statusText;
        let duration = 0;
        if (!error) {
            const typed = response;
            status = typed.status;
            statusText = typed.statusText;
            duration = typed.loadingTime;
        }
        return html `
    <ui-list-item lines="two" role="menuitem" data-key="${item.key}" data-kind="${item.kind}">
      <div class="history-label">
        <span class="label-value">${statusTemplate(status, statusText)} ${duration} ms</span>
        ${this.renderHistoryItemDelete(item)}
      </div>
      <relative-time slot="supporting-text" datetime="${isoTime}"></relative-time>
    </ui-list-item>
    `;
    }
    renderHistoryItemDelete(item) {
        return html `
    <ui-icon-button
      class="delete"
      aria-label="Delete request"
      data-key="${item.key}" 
      data-kind="${item.kind}"
      @click="${this.handleItemDelete}"
    ><ui-icon icon="deleteOutline" role="presentation"></ui-icon></ui-icon-button>
    `;
    }
    renderView() {
        const { view } = this;
        if (view === ProjectHistoryView.analysis) {
            return this.renderAnalysis();
        }
        return this.renderLog();
    }
    renderLog() {
        const { renderedItem } = this;
        if (!renderedItem) {
            return '';
        }
        const typed = renderedItem;
        const data = typed.log ? typed.log : renderedItem;
        return html `
    <http-request-log .httpLog="${data}"></http-request-log>
    `;
    }
    renderAnalysis() {
        const { model } = this;
        if (!model) {
            return '';
        }
        const chart = new HttpHistoryChart(model);
        return chart.durationPlot();
    }
    renderViewToggle() {
        return html `
    <div class="view-toggle">
      <ui-segmented-button-set @select="${this.handleViewSelection}">
        <ui-segmented-button selected title="Shows response history" data-value="history">History</ui-segmented-button>
        <ui-segmented-button title="Shows history analysis" data-value="analysis">Analysis</ui-segmented-button>
      </ui-segmented-button-set>
    </div>
    `;
    }
    renderClearHistoryDialog() {
        return html `
    <ui-dialog modal class="clear-dialog" @close="${this.handleClearHistoryDialogResult}">
      <ui-icon slot="icon" icon="deleteOutline"></ui-icon>
      <span slot="title">Delete history?</span>
      <p>This will clear responses history for this request.</p>
      <ui-button slot="button" value="dismiss" type="text">Cancel</ui-button>
      <ui-button slot="button" value="confirm" type="text">Accept</ui-button>
    </ui-dialog>
    `;
    }
}
__decorate([
    state()
], HttpProjectRequestHistory.prototype, "history", void 0);
__decorate([
    property({ type: String })
], HttpProjectRequestHistory.prototype, "project", void 0);
__decorate([
    property({ type: String })
], HttpProjectRequestHistory.prototype, "request", void 0);
__decorate([
    state()
], HttpProjectRequestHistory.prototype, "querying", void 0);
__decorate([
    state()
], HttpProjectRequestHistory.prototype, "model", void 0);
__decorate([
    state()
], HttpProjectRequestHistory.prototype, "selectedItem", void 0);
__decorate([
    state()
], HttpProjectRequestHistory.prototype, "renderedItem", void 0);
__decorate([
    state()
], HttpProjectRequestHistory.prototype, "view", void 0);
//# sourceMappingURL=HttpProjectRequestHistory.js.map