/** Dialog for completing a maintenance task with optional notes, cost, duration. */

import { LitElement, html, css, nothing } from "lit";
import { property, state } from "lit/decorators.js";
import type { HomeAssistant, TaskPartLink } from "../types";
import { t, nativeFieldStyles } from "../styles";
import { describeWsError } from "../ws-errors";
import { partLinkKey, type LinkedPart } from "../helpers/shared-parts";
import { REQUIRED_COMPLETION_LABELS } from "./required-completion-labels";

export class MaintenanceCompleteDialog extends LitElement {
  @property({ attribute: false }) public hass!: HomeAssistant;
  @property() public entryId = "";
  @property() public taskId = "";
  @property() public taskName = "";
  @property() public lang = "en";
  @property({ type: Array }) public checklist: string[] = [];
  @property({ type: Boolean }) public adaptiveEnabled = false;
  // v2.20 (#83): task type + unit drive the reading-value field below.
  @property() public taskType = "";
  @property() public readingUnit = "";
  /** Buy task (part_ref): default restock quantity — shows an editable qty field. */
  @property({ attribute: false }) public restockDefault: number | null = null;
  /** #104 follow-up: the buy task's part unit cost — powers the cost
   *  suggestion (restock qty × unit cost). */
  @property({ attribute: false }) public restockUnitCost: number | null = null;
  /** Currency symbol for the cost suggestion ("" = plain number). */
  @property() public currencySymbol = "";
  /** #99: the parts offered on completion — enables the editable "parts used"
   *  section. Built by `partsForCompletion`: the object's own inventory plus
   *  every shared pool this task links to (#111), each tagged with its owner. */
  @property({ attribute: false }) public parts: LinkedPart[] = [];
  /** #99: the task's fixed consumes_parts links (prefill for the section).
   *  A link may carry an `entry_id` (#111) and MUST keep it through the edit —
   *  without it the completion would decrement the wrong inventory, or none. */
  @property({ attribute: false }) public consumesParts: TaskPartLink[] = [];
  /** "Consumes: 1× HEPA-Filter (Shelf B)" hint lines for consuming tasks. */
  @property({ type: Array }) public consumesInfo: string[] = [];
  /** Details this task demands before it counts as done (v2.44). The backend
   *  enforces the same list at every completion surface; blocking Save here
   *  just means the user never has to meet that rejection. */
  @property({ type: Array }) public requiredFields: string[] = [];
  @state() private _open = false;
  @state() private _notes = "";
  @state() private _cost = "";
  @state() private _duration = "";
  @state() private _loading = false;
  @state() private _error = "";
  @state() private _checklistState: Record<string, boolean> = {};
  @state() private _feedback: string = "needed";
  @state() private _photoDocId = "";
  @state() private _photoPreview = "";
  @state() private _photoUploading = false;
  @state() private _readingValue = "";
  @state() private _restockQty = "";
  /** #133: optional backdated completion moment (datetime-local value; "" = now). */
  @state() private _completedAt = "";
  /** Keyed by `partLinkKey` — the (entry_id, part_id) pair — because two
   *  objects can carry the same part id, so part_id alone would merge pools. */
  @state() private _usedParts: Record<string, TaskPartLink> = {};

  /** #73: in-cycle ticks (keyed by item TEXT) recorded on the task detail —
   *  prefill the dialog so nobody re-ticks what is already done. */
  @property({ attribute: false }) public checklistPrefill: Record<string, boolean> = {};

  public open(): void {
    if (this._open) return;
    this._open = true;
    this._notes = "";
    this._cost = "";
    this._duration = "";
    this._error = "";
    // The dialog's own state is INDEX-keyed (historical shape, flows into the
    // history entry as-is) — map the text-keyed in-cycle ticks onto indices.
    this._checklistState = Object.fromEntries(
      this.checklist
        .map((item, i) => [String(i), !!this.checklistPrefill[item]] as const)
        .filter(([, done]) => done),
    );
    this._feedback = "needed";
    this._photoDocId = "";
    this._photoPreview = "";
    this._photoUploading = false;
    this._readingValue = "";
    this._restockQty = this.restockDefault !== null ? String(this.restockDefault) : "";
    this._completedAt = "";
    // #99: prefill "parts used" with the task's fixed links — the user can
    // untick or adjust before completing. The whole link is kept, entry_id
    // included, so a shared pool survives the edit (#111).
    this._usedParts = Object.fromEntries(this.consumesParts.map((l) => [partLinkKey(l), { ...l }]));
  }

  private _toggleCheck(idx: number): void {
    const key = String(idx);
    this._checklistState = {
      ...this._checklistState,
      [key]: !this._checklistState[key],
    };
  }

  private _setFeedback(value: string): void {
    this._feedback = value;
  }

  private async _onPhotoInput(e: Event): Promise<void> {
    const input = e.target as HTMLInputElement;
    const file = input.files?.[0];
    input.value = ""; // allow re-picking the same file
    if (!file) return;
    this._photoUploading = true;
    this._error = "";
    try {
      const form = new FormData();
      form.append("entry_id", this.entryId);
      form.append("tags", "photo");
      form.append("file", file, file.name);
      const resp = await fetch("/api/maintenance_supporter/document/upload", {
        method: "POST",
        headers: { Authorization: `Bearer ${this.hass.auth?.data?.access_token ?? ""}` },
        body: form,
      });
      if (!resp.ok) {
        this._error = resp.status === 413
          ? t("doc_too_large", this.lang)
          : t("doc_upload_failed", this.lang);
        return;
      }
      const doc = (await resp.json()) as { id?: string };
      if (doc.id) {
        this._photoDocId = doc.id;
        this._photoPreview = URL.createObjectURL(file);
      }
    } catch {
      this._error = t("doc_upload_failed", this.lang);
    } finally {
      this._photoUploading = false;
    }
  }

  private _removePhoto(): void {
    if (this._photoPreview) URL.revokeObjectURL(this._photoPreview);
    this._photoDocId = "";
    this._photoPreview = "";
  }

  private async _complete(): Promise<void> {
    this._loading = true;
    this._error = "";
    try {
      const data: Record<string, unknown> = {
        type: "maintenance_supporter/task/complete",
        entry_id: this.entryId,
        task_id: this.taskId,
      };
      if (this._notes) data.notes = this._notes;
      if (this._cost) {
        const cost = parseFloat(this._cost);
        if (!isNaN(cost) && cost >= 0) data.cost = cost;
      }
      if (this._duration) {
        const dur = parseInt(this._duration, 10);
        if (!isNaN(dur) && dur >= 0) data.duration = dur;
      }
      if (this.checklist.length > 0) {
        data.checklist_state = this._checklistState;
      }
      if (this.adaptiveEnabled) {
        data.feedback = this._feedback;
      }
      if (this._photoDocId) {
        data.photo_doc_id = this._photoDocId;
      }
      if (this._completedAt) {
        // Client-side guard mirrors the backend rule — a picked future moment
        // fails fast with a localized message instead of a WS roundtrip.
        if (new Date(this._completedAt).getTime() > Date.now()) {
          this._error = t("completed_at_future_error", this.lang);
          this._loading = false;
          return;
        }
        // Re-add seconds if the datetime-local input drops them (same
        // normalisation as the history-edit dialog).
        data.completed_at = this._completedAt.length === 16 ? `${this._completedAt}:00` : this._completedAt;
      }
      if (this._readingValue !== "") {
        const rv = parseFloat(this._readingValue);
        if (!isNaN(rv)) data.reading_value = rv;
      }
      if (this.restockDefault !== null && this._restockQty !== "") {
        const rq = parseFloat(this._restockQty);
        if (!isNaN(rq) && rq >= 1) data.restock_quantity = rq;
      }
      // #99: with a parts section shown, send the explicit selection — it
      // replaces the automatic consumes_parts deduction (empty = none used).
      // entry_id travels only when the pool is somebody else's (#111), so an
      // own-part payload is byte-identical to what shipped before.
      if (this.parts.length > 0) {
        data.used_parts = Object.values(this._usedParts)
          .filter((l) => Number.isFinite(l.quantity) && l.quantity > 0)
          .map((l) =>
            l.entry_id
              ? { part_id: l.part_id, quantity: l.quantity, entry_id: l.entry_id }
              : { part_id: l.part_id, quantity: l.quantity },
          );
      }
      await this.hass.connection.sendMessagePromise(data);
      this._open = false;
      this.dispatchEvent(new CustomEvent("task-completed"));
    } catch (e) {
      this._error = describeWsError(e, this.lang, t("save_error", this.lang));
    } finally {
      this._loading = false;
    }
  }

  /** Required details the user has not supplied yet (drives Save + markers). */
  private get _missingRequired(): string[] {
    const filled: Record<string, boolean> = {
      notes: this._notes.trim() !== "",
      cost: this._cost.trim() !== "",
      duration: this._duration.trim() !== "",
      photo: this._photoDocId !== "",
      // "Who did it" is filled in server-side from the authenticated
      // connection (websocket/tasks_actions.py), so the dialog satisfies it
      // as long as we ARE a logged-in user. Claiming it is always satisfied
      // was how a task requiring "user" ended up unclosable: Save stayed
      // enabled and the backend rejected the completion every time.
      user: !!this.hass?.user,
    };
    return this.requiredFields.filter((f) => !filled[f]);
  }

  /** Marker appended to a required field's label. */
  private _req(field: string) {
    return this.requiredFields.includes(field) ? html`<span class="req-mark" aria-hidden="true">*</span>` : nothing;
  }

  /** #104 follow-up: suggested cost derived from the parts this completion
   *  touches — the SELECTED "parts used" (qty × each part's unit cost) on a
   *  consuming task, or restock qty × unit cost on a buy task. Null when no
   *  involved part carries a price. Follows the live selection, so ticking
   *  a part off updates the suggestion. */
  private _partsCostSuggestion(): number | null {
    if (this.restockDefault !== null) {
      const qty = parseFloat(this._restockQty);
      if (this.restockUnitCost == null || !Number.isFinite(qty) || qty <= 0) return null;
      return Math.round(this.restockUnitCost * qty * 100) / 100;
    }
    if (!this.parts.length) return null;
    let sum = 0;
    let priced = false;
    for (const link of Object.values(this._usedParts)) {
      const def = this.parts.find(
        (pt) => partLinkKey({ part_id: pt.id, entry_id: pt.entry_id }) === partLinkKey(link),
      );
      if (def?.cost != null) {
        sum += def.cost * (link.quantity || 1);
        priced = true;
      }
    }
    return priced ? Math.round(sum * 100) / 100 : null;
  }

  /** The one-click "use ≈ X from parts" chip under the cost field. Hidden
   *  once the user typed a cost themselves — a suggestion, never an
   *  overwrite. */
  private _renderCostSuggestion(L: string) {
    if (this._cost.trim() !== "") return nothing;
    const suggestion = this._partsCostSuggestion();
    if (suggestion == null || suggestion <= 0) return nothing;
    const amount = `${suggestion.toFixed(2)}${this.currencySymbol ? ` ${this.currencySymbol}` : ""}`;
    return html`<button
      type="button"
      class="cost-suggestion"
      @click=${() => (this._cost = suggestion.toFixed(2))}
    >${t("cost_from_parts", L).replace("{amount}", amount)}</button>`;
  }

  private _close(): void {
    this._open = false;
  }

  render() {
    if (!this._open) return html``;
    const L = this.lang || this.hass?.language || "en";
    return html`
      <ha-dialog open @closed=${this._close}>
        <div class="dialog-title">${t("complete_title", L)}${this.taskName}</div>
        <div class="content">
          ${this._error ? html`<div class="error">${this._error}</div>` : nothing}
          ${this.checklist.length > 0 ? html`
            <div class="checklist-section">
              <label class="checklist-label">${t("checklist", L)}</label>
              ${this.checklist.map((item, idx) => html`
                <label class="checklist-item" @click=${() => this._toggleCheck(idx)}>
                  <input type="checkbox" .checked=${!!this._checklistState[String(idx)]} />
                  <span>${item}</span>
                </label>
              `)}
            </div>
          ` : nothing}
          ${this.taskType === "reading"
            ? html`
              <label class="field">
                <span class="field-label">${t("reading_value_label", L)}${this.readingUnit ? ` (${this.readingUnit})` : ""}</span>
                <input type="number" step="any" class="field-input"
                  .value=${this._readingValue}
                  @input=${(e: Event) => (this._readingValue = (e.target as HTMLInputElement).value)} />
              </label>`
            : nothing}
          ${this.parts.length
            ? html`<div class="used-parts">
                <span class="field-label">${t("complete_parts_used", L)}</span>
                ${this.parts.map((pt) => {
                  const key = partLinkKey({ part_id: pt.id, entry_id: pt.entry_id });
                  const link = this._usedParts[key];
                  const checked = link !== undefined;
                  const base: TaskPartLink = pt.entry_id
                    ? { part_id: pt.id, quantity: 1, entry_id: pt.entry_id }
                    : { part_id: pt.id, quantity: 1 };
                  return html`<div class="used-part-row">
                    <label class="used-part-check">
                      <input type="checkbox" .checked=${checked}
                        @change=${(e: Event) => {
                          const next = { ...this._usedParts };
                          if ((e.target as HTMLInputElement).checked) next[key] = next[key] || base;
                          else delete next[key];
                          this._usedParts = next;
                        }} />
                      <span
                        >${pt.name}${pt.owner_name
                          ? html`<span class="used-part-owner"> (${pt.owner_name})</span>`
                          : nothing}${pt.stock !== null && pt.stock !== undefined ? ` (${pt.stock}${pt.unit ? " " + pt.unit : ""})` : ""}</span
                      >
                    </label>
                    ${checked
                      ? html`<input class="used-part-qty" type="number" min="0.01" max="999" step="0.01"
                          .value=${String(link.quantity)}
                          @input=${(e: Event) => {
                            const v = parseFloat((e.target as HTMLInputElement).value);
                            this._usedParts = {
                              ...this._usedParts,
                              [key]: { ...base, quantity: Number.isFinite(v) && v >= 0.01 ? v : 1 },
                            };
                          }} />`
                      : nothing}
                  </div>`;
                })}
              </div>`
            : this.consumesInfo.length
              ? html`<div class="consumes-hint">
                  ${this.consumesInfo.map((line) => html`<div>${line}</div>`)}
                </div>`
              : nothing}
          ${this.restockDefault !== null
            ? html`
              <label class="field">
                <span class="field-label">${t("restock_quantity_label", L)}</span>
                <input type="number" step="0.01" min="0.01" class="field-input"
                  .value=${this._restockQty}
                  @input=${(e: Event) => (this._restockQty = (e.target as HTMLInputElement).value)} />
              </label>`
            : nothing}
          <!-- Native <input>s rather than <ha-textfield>: when this dialog
               is opened from a Lovelace card via dialog-mount, ha-textfield
               isn't yet registered (HA loads it lazily when its own panels
               need it) so the elements render with zero height and the user
               only sees the title + Cancel/Complete buttons — the original
               bug report. Native inputs always render. -->
          <label class="field">
            <span class="field-label">${t("notes_optional", L)}${this._req("notes")}</span>
            <input type="text" class="field-input"
              .value=${this._notes}
              @input=${(e: Event) => (this._notes = (e.target as HTMLInputElement).value)} />
          </label>
          <label class="field">
            <span class="field-label">${t("cost_optional", L)}${this._req("cost")}</span>
            <input type="number" step="0.01" min="0" class="field-input"
              .value=${this._cost}
              @input=${(e: Event) => (this._cost = (e.target as HTMLInputElement).value)} />
            ${this._renderCostSuggestion(L)}
          </label>
          <label class="field">
            <span class="field-label">${t("duration_minutes", L)}${this._req("duration")}</span>
            <input type="number" step="0.01" min="0" class="field-input"
              .value=${this._duration}
              @input=${(e: Event) => (this._duration = (e.target as HTMLInputElement).value)} />
          </label>
          <label class="field">
            <span class="field-label">${t("completed_at_optional", L)}</span>
            <input type="datetime-local" class="field-input"
              max=${new Date(Date.now() - new Date().getTimezoneOffset() * 60000).toISOString().slice(0, 16)}
              .value=${this._completedAt}
              @change=${(e: Event) => (this._completedAt = (e.target as HTMLInputElement).value)} />
          </label>
          <div class="field">
            <span class="field-label">${t("completion_photo_optional", L)}${this._req("photo")}</span>
            ${this._photoPreview
              ? html`
                <div class="photo-preview">
                  <img src=${this._photoPreview} alt="" />
                  <button type="button" class="photo-remove" @click=${this._removePhoto}
                    title="${t("remove", L)}">✕</button>
                </div>`
              : html`
                <label class="photo-pick">
                  <ha-icon icon="mdi:camera"></ha-icon>
                  <span>${this._photoUploading ? t("uploading", L) : t("add_photo", L)}</span>
                  <input type="file" accept="image/*" capture="environment"
                    ?disabled=${this._photoUploading}
                    @change=${this._onPhotoInput} />
                </label>`}
          </div>
          ${this.adaptiveEnabled ? html`
            <div class="feedback-section">
              <label class="feedback-label">${t("was_maintenance_needed", L)}</label>
              <div class="feedback-buttons">
                <button
                  class="feedback-btn ${this._feedback === "needed" ? "selected" : ""}"
                  @click=${() => this._setFeedback("needed")}
                >${t("feedback_needed", L)}</button>
                <button
                  class="feedback-btn ${this._feedback === "not_needed" ? "selected" : ""}"
                  @click=${() => this._setFeedback("not_needed")}
                >${t("feedback_not_needed", L)}</button>
                <button
                  class="feedback-btn ${this._feedback === "not_sure" ? "selected" : ""}"
                  @click=${() => this._setFeedback("not_sure")}
                >${t("feedback_not_sure", L)}</button>
              </div>
            </div>
          ` : nothing}
        </div>
        <div class="dialog-actions">
          <ha-button appearance="plain" @click=${this._close}>
            ${t("cancel", L)}
          </ha-button>
          <ha-button
            @click=${this._complete}
            .disabled=${this._loading || this._missingRequired.length > 0}
            title=${this._missingRequired.length
              ? this._missingRequired.map((f) => t("err_required", L).replace("{field}", t(REQUIRED_COMPLETION_LABELS[f] ?? f, L))).join(" · ")
              : ""}
          >
            ${this._loading ? t("completing", L) : t("complete", L)}
          </ha-button>
        </div>
      </ha-dialog>
    `;
  }

  static styles = [nativeFieldStyles, css`
    .req-mark {
      color: var(--error-color, #f44336);
      margin-left: 2px;
      font-weight: 600;
    }
    /* #104: one-click cost suggestion from parts — quiet link-style chip. */
    .cost-suggestion {
      align-self: flex-start;
      margin-top: 4px;
      padding: 0;
      border: none;
      background: none;
      color: var(--primary-color);
      font-size: 12.5px;
      cursor: pointer;
      text-decoration: underline dotted;
      text-underline-offset: 2px;
    }
    .dialog-title {
      font-size: 18px;
      font-weight: 500;
      padding-bottom: 12px;
    }
    .content {
      display: flex;
      flex-direction: column;
      gap: 16px;
      min-width: 300px;
    }
    .dialog-actions {
      display: flex;
      justify-content: flex-end;
      gap: 8px;
      padding-top: 16px;
    }
    .consumes-hint {
      font-size: 13px;
      color: var(--secondary-text-color);
      border-left: 3px solid var(--primary-color);
      padding: 4px 8px;
      margin: 4px 0 8px;
    }
    /* #99: editable per-completion parts selection */
    .used-parts { margin: 4px 0 8px; display: flex; flex-direction: column; gap: 4px; }
    .used-part-row { display: flex; align-items: center; gap: 8px; }
    .used-part-check {
      display: flex; align-items: center; gap: 6px; flex: 1;
      font-size: 13px; cursor: pointer;
    }
    .used-part-check input { cursor: pointer; }
    /* #111: whose stock this row draws on. Muted but never omitted — an
       unlabelled foreign pool is indistinguishable from an own part. */
    .used-part-owner { color: var(--secondary-text-color); }
    .used-part-qty {
      width: 76px; padding: 4px 6px; border-radius: 4px; font: inherit; font-size: 13px;
      border: 1px solid var(--divider-color);
      background: var(--card-background-color);
      color: var(--primary-text-color);
    }
    .error {
      color: var(--error-color, #f44336);
      font-size: 13px;
    }
    /* .field/.field-label/.field-input come from nativeFieldStyles */
    .photo-pick {
      display: inline-flex;
      align-items: center;
      gap: 8px;
      padding: 8px 12px;
      border: 1px dashed var(--divider-color);
      border-radius: 8px;
      cursor: pointer;
      font-size: 13px;
      color: var(--secondary-text-color);
      width: fit-content;
    }
    .photo-pick:hover { border-color: var(--primary-color); }
    .photo-pick input[type="file"] { display: none; }
    .photo-preview {
      position: relative;
      width: fit-content;
    }
    .photo-preview img {
      max-width: 160px;
      max-height: 160px;
      border-radius: 8px;
      display: block;
    }
    .photo-remove {
      position: absolute;
      top: -8px;
      right: -8px;
      width: 24px;
      height: 24px;
      border-radius: 50%;
      border: none;
      background: var(--error-color, #db4437);
      color: #fff;
      cursor: pointer;
      font-size: 12px;
      line-height: 1;
    }
    .checklist-section {
      display: flex;
      flex-direction: column;
      gap: 8px;
      padding: 8px 0;
      border-bottom: 1px solid var(--divider-color);
      margin-bottom: 4px;
    }
    .checklist-label {
      font-weight: 500;
      font-size: 13px;
      color: var(--secondary-text-color);
    }
    .checklist-item {
      display: flex;
      align-items: center;
      gap: 8px;
      cursor: pointer;
      padding: 4px 0;
      font-size: 14px;
    }
    .checklist-item input[type="checkbox"] {
      width: 18px;
      height: 18px;
      cursor: pointer;
    }
    .feedback-section {
      display: flex;
      flex-direction: column;
      gap: 8px;
      padding: 8px 0;
      border-top: 1px solid var(--divider-color);
    }
    .feedback-label {
      font-weight: 500;
      font-size: 13px;
      color: var(--secondary-text-color);
    }
    .feedback-buttons {
      display: flex;
      gap: 8px;
    }
    .feedback-btn {
      flex: 1;
      padding: 8px 12px;
      border: 1px solid var(--divider-color);
      border-radius: 8px;
      background: var(--card-background-color, #fff);
      color: var(--primary-text-color);
      font-size: 13px;
      cursor: pointer;
      text-align: center;
      transition: all 0.2s;
    }
    .feedback-btn:hover {
      background: var(--secondary-background-color, #f5f5f5);
    }
    .feedback-btn.selected {
      background: var(--primary-color);
      color: var(--text-primary-color, #fff);
      border-color: var(--primary-color);
    }
  `];
}

// Safe registration — avoids duplicate define when both panel and card load
if (!customElements.get("maintenance-complete-dialog")) {
  customElements.define("maintenance-complete-dialog", MaintenanceCompleteDialog);
}
