/**
 * Copyright 2026 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {LitElement, css, html, nothing} from 'lit';
import {customElement, state} from 'lit/decorators.js';
import {FORM_STYLES} from '../css/forms-css.js';
import {SHARED_STYLES} from '../css/shared-css.js';
import {showToastMessage} from './utils.js';
import {parse} from 'csv-parse/browser/esm/sync';

type Item = {
  row: number;
  entryName?: string;
  csFid: number;
  existing?: string;
  desired: string;
};

const WEB_FEATURE_ID_RE = new RegExp('^[a-z0-9]+(-[a-z0-9]+)*$');
const CHROMESTATUS_URL_RE = new RegExp(
  'https://(wwww.)?chromestatus.com/feature/(?<id>[0-9]+)'
);

@customElement('chromedash-bulk-edit-page')
export class ChromedashBulkEditPage extends LitElement {
  static get styles() {
    return [
      ...SHARED_STYLES,
      ...FORM_STYLES,
      css`
        section {
          margin: var(--content-padding);
        }
        .diff.old {
          background: #fdd;
        }
        .diff.new {
          background: #dfd;
        }
      `,
    ];
  }

  @state()
  parsing = false;

  @state()
  cells: string[][] = [];

  @state()
  featureIdIndex: number = -1;

  @state()
  chromestatusUrlIndex: number = -1;

  @state()
  items: Item[] = [];

  @state()
  submitting = false;

  parseChromestatusIdFromURL(url: string): number {
    const match = url.match(CHROMESTATUS_URL_RE);
    if (match) {
      return Number(match.groups?.id);
    }
    const num = Number(url);
    return isNaN(num) ? 0 : num;
  }

  isFeatureIdHeader(header: string, index: number) {
    const headerOk =
      header === 'feature id' ||
      header === 'web-features' ||
      (header.includes('feature') && !header.includes('chrome'));
    const firstValueOk =
      this.cells.length > 1 && this.cells[1][index].match(WEB_FEATURE_ID_RE);
    return headerOk && firstValueOk;
  }

  isChromestatusURL(header: string, index: number) {
    const headerOk =
      header.includes('chromestatus') ||
      (header.includes('chrome') && header.includes('status')) ||
      header === 'id';
    const firstValueOk =
      this.cells.length > 1 &&
      this.parseChromestatusIdFromURL(this.cells[1][index]);
    return headerOk && firstValueOk;
  }

  detectColumns() {
    if (!this.cells.length) {
      return;
    }
    const headerCells: string[] = this.cells[0].map(header =>
      header.toLowerCase()
    );
    this.featureIdIndex = headerCells.findIndex(
      this.isFeatureIdHeader.bind(this)
    );
    this.chromestatusUrlIndex = headerCells.findIndex(
      this.isChromestatusURL.bind(this)
    );
  }

  selectItems() {
    this.items = [];
    if (this.featureIdIndex === -1 || this.chromestatusUrlIndex === -1) {
      return;
    }
    for (let i = 1; i < this.cells.length; i++) {
      const row = this.cells[i];
      const webFeatureId: string = row[this.featureIdIndex];
      const csUrl = row[this.chromestatusUrlIndex];
      const csFid: number = this.parseChromestatusIdFromURL(csUrl);
      if (csFid > 0) {
        const item: Item = {
          row: i + 1,
          csFid: csFid,
          existing: 'Loading...',
          desired: webFeatureId,
        };
        this.items.push(item);
      }
    }
  }

  fetchFeature(item: Item): Promise<void> {
    return window.csClient.getFeature(item.csFid).then(fe => {
      item.entryName = fe.name;
      item.existing = fe.web_feature;
      this.requestUpdate(); // Render page to show progress.
    });
  }

  parseFileContent(fileContent: string) {
    try {
      this.cells = parse(fileContent);
    } catch (error) {
      showToastMessage(
        `An error occurred in parsing CSV file. ${JSON.stringify(error)}`
      );
    }
    try {
      this.detectColumns();
    } catch (error) {
      showToastMessage(
        `An error occurred while detecting columns. ${JSON.stringify(error)}`
      );
    }
    try {
      this.selectItems();
    } catch (error) {
      showToastMessage(
        `An error occurred while converting cells. ${JSON.stringify(error)}`
      );
    }
  }

  async getFileAndParse() {
    const fileField = this.shadowRoot!.querySelector<HTMLInputElement>(
      '#id_file_form_field'
    );
    if (!fileField?.files?.length) {
      // The user canceled the file selection dialog, or selected an empty file.
      this.cells = [];
      this.featureIdIndex = -1;
      this.chromestatusUrlIndex = -1;
      this.items = [];
      this.parsing = false;
      return;
    }
    try {
      const file = fileField.files[0];
      const fileContent = await file.text();
      this.parseFileContent(fileContent);
      await Promise.all(this.items.map(item => this.fetchFeature(item)));
    } catch (error) {
      showToastMessage(
        `An error occurred while processing the file. ${JSON.stringify(error)}`
      );
    } finally {
      this.parsing = false;
    }
  }

  saveFeature(item: Item): Promise<void> {
    const submitBody = {
      feature_changes: {
        id: item.csFid,
        web_feature: item.desired,
      },
      stages: [],
      has_changes: true,
    };
    return window.csClient
      .updateFeature(submitBody)
      .then(resp => {
        this.fetchFeature(item);
      })
      .catch(() => {
        showToastMessage(
          'Some errors occurred. Please refresh the page or try again later.'
        );
      });
  }

  async handleSubmit(e: PointerEvent) {
    e.preventDefault();
    this.submitting = true;
    for (const item of this.items) {
      if (item.existing != item.desired) {
        // Save one at a time to avoid a load spike.
        await this.saveFeature(item);
      }
    }
    this.submitting = false;
  }

  async handleChange() {
    this.parsing = true;
    await this.getFileAndParse();
  }

  renderForm() {
    return html`
      <form name="bulk_edit_form">
        <table cellspacing="6">
          <tbody>
            <tr>
              <th>
                <label for="id_file_form_field">CSV file:</label>
              </th>
              <td>
                <input
                  id="id_file_form_field"
                  name="file_form_field"
                  type="file"
                  accept="text/csv"
                  @change=${this.handleChange}
                />
              </td>
            </tr>
          </tbody>
        </table>
      </form>
    `;
  }

  isDifferent(item: Item): boolean {
    return item.existing != 'Loading...' && item.existing != item.desired;
  }

  renderItemRow(item: Item) {
    const different = this.isDifferent(item);
    const differentClass = different ? 'diff' : '';
    return html`
      <tr>
        <td style="text-align: right; width: 4em">${item.row}</td>
        <td>
          <a href="/feature/${item.csFid}" target="_blank"
            >${item.entryName || item.csFid}</a
          >
        </td>
        <td class="${differentClass} old">${item.existing || 'Not set'}</td>
        <td>${different ? html`&larr;` : '=='}</td>
        <td class="${differentClass} new">${item.desired}</td>
      </tr>
    `;
  }

  renderPreview() {
    if (this.items.length == 0) {
      return html`
        <p id="instructions">
          Select a CSV file to see the preview. The CSV file should have one
          column labeled "Chrome Status Entry" and one column labled "Feature
          ID", although some variations may work. Once the file is parsed, the
          table below will preview changes that will be made.
        </p>
      `;
    }
    const totalCount = this.items.length;
    const matchCount = this.items.filter(
      item => !this.isDifferent(item)
    ).length;
    const updateCount = this.items.filter(item =>
      this.isDifferent(item)
    ).length;
    return html`
      <table class="data-table">
        <tr>
          <th>Row</th>
          <th>ChromeStatus feature</th>
          <th>Existing web feature ID</th>
          <th></th>
          <th>Desired web feature ID</th>
        </tr>
        ${this.items.map(item => this.renderItemRow(item))}
      </table>
      <p>
        ${totalCount} rows: ${matchCount} already matching and ${updateCount} to
        be updated.
      </p>
    `;
  }

  renderControls() {
    return html`
      <sl-button
        ?disabled=${this.parsing || this.submitting || this.items.length == 0}
        @click=${this.handleSubmit}
        size="small"
        variant="primary"
      >
        Update all
      </sl-button>
    `;
  }

  render() {
    return html`
      <div id="subheader">
        <h2>Bulk edit</h2>
      </div>
      <section>${this.renderForm()}</section>
      <section>
        <h3>Preview</h3>
        ${this.renderPreview()}
      </section>
      <section>${this.renderControls()}</section>
      ${
        this.parsing || this.submitting
          ? html` <div class="loading">
              <div id="spinner"><img src="/static/img/ring.svg" /></div>
            </div>`
          : nothing
      }
    `;
  }
}
