/** Documents linked to a maintenance task — or (v2.26) to a spare part.
 *
 * A filtered view over the object's document pool via each doc's `task_ids`
 * (task mode) or `part_ids` (part mode, set `partId` instead of `taskId`):
 * link/unlink existing object documents and open/download them. Full upload +
 * management lives at the object level (documents-section); this keeps the
 * manual / datasheet right where the work happens. Per-task PDF page hints
 * exist only in task mode. Hides itself entirely when the object has no
 * documents at all.
 */

import { LitElement, html, css, nothing } from "lit";
import { property, state } from "lit/decorators.js";
import { t, ensureLocale, langOf } from "../styles";
import { describeWsError } from "../ws-errors";
import { downloadUrl } from "../helpers/download";
import { downloadSignedDocument, openSignedDocument } from "../helpers/document-url";
import { isSafeHttpUrl } from "../helpers/url";
import { formatBytes } from "../helpers/format-bytes";
import { docDisplayName, CATEGORIES, CATEGORY_ICONS } from "../helpers/document-categories";
import type { HomeAssistant } from "../types";

interface Doc {
  id: string;
  kind: "file" | "weblink";
  title: string;
  filename?: string;
  url?: string;
  mime?: string;
  size?: number;
  tags?: string[];
  task_ids?: string[];
  task_pages?: Record<string, number>;
  part_ids?: string[];
}

export class MaintenanceTaskDocuments extends LitElement {
  @property({ attribute: false }) public hass!: HomeAssistant;
  @property({ attribute: false }) public entryId!: string;
  @property({ attribute: false }) public taskId?: string;
  /** Part mode: link docs to this spare part (via `part_ids`) instead of a task. */
  @property({ attribute: false }) public partId?: string;
  @property({ type: Boolean }) public canWrite = false;

  @state() private _docs: Doc[] = [];
  @state() private _loaded = false;
  @state() private _busy = false;
  @state() private _error = "";
  @state() private _attachId = "";

  private _loadedKey = "";
  private _localeReady = false;

  private get _lang(): string {
    return langOf(this.hass);
  }

  /** The id linked docs reference — a task id or (part mode) a part id. */
  private get _refId(): string {
    return this.partId || this.taskId || "";
  }

  /** The doc metadata field the link lives in. */
  private get _linkField(): "task_ids" | "part_ids" {
    return this.partId ? "part_ids" : "task_ids";
  }

  updated(changed: Map<string, unknown>): void {
    super.updated(changed);
    if (this.hass && !this._localeReady) {
      this._localeReady = true;
      void ensureLocale(this._lang).then(() => this.requestUpdate());
    }
    const key = `${this.entryId}|${this._refId}`;
    if (this.hass && this.entryId && this._refId && this._loadedKey !== key) {
      this._loadedKey = key;
      void this._load();
    }
  }

  private async _load(): Promise<void> {
    try {
      const r = await this.hass.connection.sendMessagePromise<{ documents: Doc[] }>({
        type: "maintenance_supporter/documents/list",
        entry_id: this.entryId,
      });
      this._docs = r.documents || [];
      this._loaded = true;
      this._error = "";
    } catch (e) {
      this._error = describeWsError(e, this._lang);
      this._loaded = true;
    }
  }

  private _links(doc: Doc): string[] {
    return doc[this._linkField] || [];
  }

  private _linked(): Doc[] {
    return this._docs.filter((d) => this._links(d).includes(this._refId));
  }

  private _available(): Doc[] {
    return this._docs.filter((d) => !this._links(d).includes(this._refId));
  }

  private async _setLinks(doc: Doc, ids: string[]): Promise<void> {
    this._busy = true;
    this._error = "";
    try {
      await this.hass.connection.sendMessagePromise({
        type: "maintenance_supporter/documents/update",
        doc_id: doc.id,
        [this._linkField]: ids,
      });
      await this._load();
    } catch (e) {
      this._error = describeWsError(e, this._lang);
    } finally {
      this._busy = false;
    }
  }

  private _link(): void {
    const doc = this._docs.find((d) => d.id === this._attachId);
    if (!doc) return;
    this._attachId = "";
    void this._setLinks(doc, [...this._links(doc), this._refId]);
  }

  private _unlink(doc: Doc): void {
    void this._setLinks(doc, this._links(doc).filter((x) => x !== this._refId));
  }

  private _isPdf(doc: Doc): boolean {
    return doc.mime === "application/pdf" || (doc.filename || "").toLowerCase().endsWith(".pdf");
  }

  /** The page this doc should open at for the current task, if set (PDFs only;
   *  page hints are a task-mode concept — none in part mode). */
  private _pageFor(doc: Doc): number | undefined {
    return this._isPdf(doc) && this.taskId ? doc.task_pages?.[this.taskId] : undefined;
  }

  private async _open(doc: Doc): Promise<void> {
    if (doc.kind === "weblink") {
      // Defense-in-depth: the add-link WS path already refuses non-http(s)
      // schemes, but a weblink stored before that check (or via a future
      // path) must never reach window.open as javascript:/data: — guard the
      // sink too, like documents-section does.
      if (isSafeHttpUrl(doc.url)) window.open(doc.url, "_blank", "noopener");
      return;
    }
    // A per-task page hint jumps straight to the relevant page via the PDF
    // viewer's #page=N fragment (client-side, so it never breaks the signature).
    const page = this._pageFor(doc);
    try {
      await openSignedDocument(this.hass, doc.id, page ? `#page=${page}` : "");
    } catch (e) {
      this._error = describeWsError(e, this._lang);
    }
  }

  /** Set (page >= 1) or clear (0) the jump-to page for this doc's task link. */
  private async _setPage(doc: Doc, page: number): Promise<void> {
    if (!this.taskId) return;
    this._busy = true;
    this._error = "";
    try {
      await this.hass.connection.sendMessagePromise({
        type: "maintenance_supporter/documents/update",
        doc_id: doc.id,
        task_pages: { [this.taskId]: page },
      });
      await this._load();
    } catch (e) {
      this._error = describeWsError(e, this._lang);
    } finally {
      this._busy = false;
    }
  }

  private async _download(doc: Doc): Promise<void> {
    try {
      await downloadSignedDocument(this.hass, doc.id, doc.filename || doc.title || "document");
    } catch (e) {
      this._error = describeWsError(e, this._lang);
    }
  }

  render() {
    // Hide entirely for objects with no documents — no clutter on every task.
    if (!this._loaded || this._docs.length === 0) return nothing;
    const L = this._lang;
    const linked = this._linked();
    const available = this._available();
    return html`
      <div class="task-docs">
        <h3><ha-icon icon="mdi:paperclip"></ha-icon> ${t("documents", L)} (${linked.length})</h3>
        ${this._error ? html`<div class="tdoc-error">${this._error}</div>` : nothing}
        ${linked.length === 0
          ? html`<div class="tdoc-empty">${t(this.partId ? "doc_part_none" : "doc_task_none", L)}</div>`
          : html`<div class="tdoc-list">${linked.map((d) => this._renderRow(d, L))}</div>`}
        ${this.canWrite && available.length
          ? html`<div class="tdoc-attach">
              <select
                class="tdoc-select"
                ?disabled=${this._busy}
                @change=${(e: Event) => (this._attachId = (e.target as HTMLSelectElement).value)}
              >
                <option value="" ?selected=${!this._attachId}>${t("doc_link_existing", L)}</option>
                ${available.map(
                  (d) => html`<option value=${d.id} ?selected=${d.id === this._attachId}>${docDisplayName(d)}</option>`,
                )}
              </select>
              <button class="tdoc-btn" ?disabled=${this._busy || !this._attachId} @click=${this._link}>
                <ha-icon icon="mdi:link-variant-plus"></ha-icon> ${t("doc_attach", L)}
              </button>
            </div>`
          : nothing}
      </div>
    `;
  }

  private _renderRow(doc: Doc, L: string) {
    const isFile = doc.kind === "file";
    const isPdf = this._isPdf(doc);
    const page = this._pageFor(doc);
    const cat = (doc.tags || []).find((x) => (CATEGORIES as readonly string[]).includes(x)) || "other";
    const meta = isFile ? formatBytes(doc.size) : t("doc_link_badge", L);
    return html`
      <div class="tdoc-row">
        <ha-icon class="tdoc-icon" icon=${isFile ? CATEGORY_ICONS[cat] : "mdi:link-variant"}></ha-icon>
        <div
          class="tdoc-info"
          role="button"
          tabindex="0"
          title=${page ? `${t("doc_open", L)} · ${t("doc_page", L)} ${page}` : t("doc_open", L)}
          @click=${() => this._open(doc)}
          @keydown=${(e: KeyboardEvent) => {
            if (e.key === "Enter" || e.key === " ") {
              e.preventDefault();
              void this._open(doc);
            }
          }}
        >
          <div class="tdoc-title">${docDisplayName(doc)}</div>
          <div class="tdoc-meta">
            ${meta}${page ? html` · <span class="tdoc-pagetag">${t("doc_page", L)} ${page}</span>` : nothing}
          </div>
        </div>
        ${this.canWrite && isPdf && this.taskId
          ? html`<input
              class="tdoc-page"
              type="number"
              min="1"
              inputmode="numeric"
              aria-label=${t("doc_page", L)}
              title=${t("doc_page", L)}
              placeholder=${t("doc_page", L)}
              .value=${page ? String(page) : ""}
              ?disabled=${this._busy}
              @change=${(e: Event) => {
                const v = parseInt((e.target as HTMLInputElement).value, 10);
                void this._setPage(doc, Number.isFinite(v) && v >= 1 ? v : 0);
              }}
            />`
          : nothing}
        <button class="icon-btn" title=${t("doc_open", L)} @click=${() => this._open(doc)}>
          <ha-icon icon=${isFile ? "mdi:eye-outline" : "mdi:open-in-new"}></ha-icon>
        </button>
        ${isFile
          ? html`<button class="icon-btn" title=${t("doc_download", L)} @click=${() => this._download(doc)}>
              <ha-icon icon="mdi:download"></ha-icon>
            </button>`
          : nothing}
        ${this.canWrite
          ? html`<button class="icon-btn" title=${t("doc_unlink", L)} ?disabled=${this._busy} @click=${() => this._unlink(doc)}>
              <ha-icon icon="mdi:link-variant-off"></ha-icon>
            </button>`
          : nothing}
      </div>
    `;
  }

  static styles = css`
    :host { display: block; }
    .task-docs { margin-top: 20px; }
    h3 {
      display: flex; align-items: center; gap: 6px; margin: 0 0 8px;
      font-size: 15px; color: var(--primary-text-color);
    }
    h3 ha-icon { --mdc-icon-size: 18px; color: var(--secondary-text-color, #888); }
    .tdoc-empty { color: var(--secondary-text-color, #888); font-size: 13px; padding: 2px 0 8px; }
    .tdoc-error { color: var(--error-color, #f44336); font-size: 13px; margin: 4px 0; }
    .tdoc-list { display: flex; flex-direction: column; gap: 4px; }
    .tdoc-row {
      display: flex; align-items: center; gap: 10px; padding: 6px 10px;
      border: 1px solid var(--divider-color); border-radius: 8px;
    }
    .tdoc-icon { color: var(--primary-color); --mdc-icon-size: 22px; flex: none; }
    .tdoc-info { flex: 1; min-width: 0; cursor: pointer; border-radius: 6px; }
    .tdoc-info:hover .tdoc-title { text-decoration: underline; }
    .tdoc-info:focus-visible { outline: 2px solid var(--primary-color); outline-offset: 2px; }
    .tdoc-title { font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .tdoc-meta { font-size: 12px; color: var(--secondary-text-color, #888); }
    .tdoc-pagetag { color: var(--primary-color); font-weight: 500; }
    .tdoc-page {
      flex: none; width: 76px; padding: 5px 8px; border-radius: 6px; font: inherit; font-size: 13px;
      background: var(--secondary-background-color, rgba(0, 0, 0, 0.06));
      color: var(--primary-text-color); border: 1px solid var(--divider-color);
    }
    .tdoc-page:disabled { opacity: 0.5; }
    .icon-btn {
      display: inline-flex; align-items: center; justify-content: center;
      width: 34px; height: 34px; border-radius: 8px; cursor: pointer;
      background: transparent; border: none; color: var(--primary-text-color);
    }
    .icon-btn:hover { background: var(--secondary-background-color, rgba(0, 0, 0, 0.06)); }
    .icon-btn[disabled] { opacity: 0.4; pointer-events: none; }
    .icon-btn ha-icon { --mdc-icon-size: 20px; }
    .tdoc-attach { display: flex; gap: 8px; margin-top: 8px; flex-wrap: wrap; }
    .tdoc-select {
      flex: 1; min-width: 160px; padding: 6px 10px; border-radius: 6px; font: inherit;
      background: var(--secondary-background-color, rgba(0, 0, 0, 0.06));
      color: var(--primary-text-color); border: 1px solid var(--divider-color);
    }
    .tdoc-btn {
      display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
      padding: 6px 12px; border-radius: 6px; font: inherit; font-size: 13px;
      background: var(--secondary-background-color, rgba(0, 0, 0, 0.06));
      color: var(--primary-text-color); border: 1px solid var(--divider-color);
    }
    .tdoc-btn ha-icon { --mdc-icon-size: 18px; }
    .tdoc-btn[disabled] { opacity: 0.5; pointer-events: none; }
  `;
}

if (!customElements.get("maintenance-task-documents")) {
  customElements.define("maintenance-task-documents", MaintenanceTaskDocuments);
}
