/** Maintenance Supporter — custom dashboard strategy.
 *
 * Generates a complete Lovelace dashboard from the integration's WS feed.
 * Honors a ``group_by`` config so YAML users can pick the layout:
 *
 *   strategy:
 *     type: custom:maintenance-supporter
 *     group_by: area      # default — one view per area + Unassigned
 *
 *   strategy:
 *     type: custom:maintenance-supporter
 *     group_by: status    # one view per status (Overdue / Triggered / Due Soon / OK)
 *
 *   strategy:
 *     type: custom:maintenance-supporter
 *     group_by: floor     # one view per floor with area sub-headings (#155 follow-up)
 *
 *   strategy:
 *     type: custom:maintenance-supporter
 *     group_by: due_date  # 5 buckets — Overdue / Today / This Week / This Month / Later
 *
 * Layout pattern follows HA's own ``maintenance-view-strategy``,
 * ``areas-dashboard-strategy`` and ``home-overview-view-strategy``: a leading
 * "Overview" view of actionable tasks, empty groups skipped, and
 * STATE_NOT_RUNNING / recovery_mode handled with the same starting /
 * recovery-mode card placeholders HA core uses.
 *
 * Requires Home Assistant 2026.5+ to appear in the "Add Dashboard" picker
 * (that's when ``window.customStrategies`` got picked up by the frontend).
 * On older HA versions the registration is a silent no-op.
 */

import { STATUS_ICONS } from "./status-constants";

interface MaintenanceObjectResp {
  entry_id: string;
  object: {
    id: string;
    name: string;
    area_id: string | null;
  };
  tasks: Array<{ status?: string; days_until_due?: number | null }>;
}

interface AreaEntry {
  area_id: string;
  name: string;
  icon?: string | null;
  floor_id?: string | null;
}

interface FloorEntry {
  floor_id: string;
  name: string;
  icon?: string | null;
  level?: number | null;
}

interface HassLike {
  language?: string;
  config?: {
    state?: string;
    recovery_mode?: boolean;
  };
  connection: {
    sendMessagePromise<T>(msg: Record<string, unknown>): Promise<T>;
  };
  areas?: Record<string, AreaEntry>;
  floors?: Record<string, FloorEntry>;
}

type GroupBy = "area" | "status" | "floor" | "due_date" | "calendar";

interface MaintenanceDashboardStrategyConfig {
  type: "custom:maintenance-supporter" | "maintenance-supporter";
  group_by?: GroupBy;
}

interface DashboardConfig {
  title?: string;
  views: ViewConfig[];
}

interface ViewConfig {
  title?: string;
  path?: string;
  icon?: string;
  type?: string;
  subview?: boolean;
  cards?: CardConfig[];
  sections?: SectionConfig[];
  // HA 2026.5+ — banner card above the view (mobile + desktop), and a
  // narrow column on the right (large screens only). See
  // home-overview-view-strategy.ts for the canonical usage.
  header?: { layout?: string; card: CardConfig };
  sidebar?: {
    sections: SectionConfig[];
    visibility?: ViewColumnsCondition[];
    content_label?: string;
    sidebar_label?: string;
  };
  max_columns?: number;
}

interface ViewColumnsCondition {
  condition: "view_columns";
  min?: number;
  max?: number;
}

interface SectionConfig {
  type?: string;
  cards: CardConfig[];
}

interface CardConfig {
  type: string;
  [key: string]: unknown;
}

const STRATEGY_TYPE = "maintenance-supporter";
const STRATEGY_TAG = `ll-strategy-dashboard-${STRATEGY_TYPE}`;
const EDITOR_TAG = "hui-maintenance-supporter-strategy-editor";

const STATE_NOT_RUNNING = "NOT_RUNNING";

const STATUS_VIEWS: Array<{
  status: string;
  title: string;
  icon: string;
  path: string;
}> = [
  // Icons come from the shared STATUS_ICONS so the generated dashboard
  // matches the panel (this table used to hardcode its own set — "Overdue"
  // wore the panel's due-soon icon).
  { status: "overdue", title: "Overdue", icon: STATUS_ICONS.overdue, path: "overdue" },
  { status: "triggered", title: "Triggered", icon: STATUS_ICONS.triggered, path: "triggered" },
  { status: "due_soon", title: "Due Soon", icon: STATUS_ICONS.due_soon, path: "due-soon" },
  { status: "ok", title: "OK", icon: STATUS_ICONS.ok, path: "ok" },
];

const DUE_DATE_VIEWS: Array<{
  title: string;
  icon: string;
  path: string;
  filter: { filter_due_min_days?: number; filter_due_max_days?: number };
  // Pre-check predicate so we can skip empty buckets without a card render.
  matches: (days: number) => boolean;
}> = [
  {
    title: "Overdue",
    icon: "mdi:alert-circle",
    path: "overdue",
    filter: { filter_due_max_days: -1 },
    matches: (d) => d <= -1,
  },
  {
    title: "Today",
    icon: "mdi:calendar-today",
    path: "today",
    filter: { filter_due_min_days: 0, filter_due_max_days: 0 },
    matches: (d) => d === 0,
  },
  {
    title: "This Week",
    icon: "mdi:calendar-week",
    path: "this-week",
    filter: { filter_due_min_days: 1, filter_due_max_days: 7 },
    matches: (d) => d >= 1 && d <= 7,
  },
  {
    title: "This Month",
    icon: "mdi:calendar-month",
    path: "this-month",
    filter: { filter_due_min_days: 8, filter_due_max_days: 30 },
    matches: (d) => d >= 8 && d <= 30,
  },
  {
    title: "Later",
    icon: "mdi:calendar-clock",
    path: "later",
    filter: { filter_due_min_days: 31 },
    matches: (d) => d >= 31,
  },
];

function makeCardSection(card: CardConfig): SectionConfig {
  return { type: "grid", cards: [card] };
}

// Replicate HA core's responsive conditions (see panels/lovelace/strategies/
// helpers/view-columns-conditions.ts). They use the "view_columns" condition
// rather than raw media queries so they follow whatever breakpoint the user's
// view actually has.
const LARGE_SCREEN_CONDITION: ViewColumnsCondition = {
  condition: "view_columns",
  min: 2,
};
const SMALL_SCREEN_CONDITION: ViewColumnsCondition = {
  condition: "view_columns",
  max: 1,
};

// Headline counts come from the global summary sensors — the single source of
// truth shared with the panel KPI chips and the statistics WS endpoint — so the
// numbers can't drift. Rendered live via a markdown template instead of being
// counted client-side at generation time (which froze them until reload).
// (#86) Entity ids come registry-resolved from the statistics WS: the
// documented sensor.maintenance_supporter_<key> ids are NOT guaranteed —
// a user rename, a _2 collision suffix, or a pre-pinning install that
// registered localized ids all made the hardcoded templates read "unknown".
type SummaryKey = "overdue" | "triggered" | "due_soon" | "ok";
type SummaryEntityIds = Partial<Record<SummaryKey, string | null>>;
type SummaryCounts = Partial<Record<SummaryKey, number>>;

// One chip's value: prefer the live summary sensor (reactive template) when its
// entity exists, but fall back to the literal count from the statistics WS when
// it doesn't. The summary sensors only exist while the global "Maintenance
// Supporter" entry is set up; if it was deleted (orphan object entries — #86,
// 2nd report) the templated `states()` reads "unknown". The count from the WS
// is always authoritative, so the chips stay correct either way.
function kpiValue(key: SummaryKey, ids: SummaryEntityIds, counts: SummaryCounts): string {
  const id = ids[key];
  if (id) return `{{ states('${id}') }}`;
  const n = counts[key];
  return n === undefined || n === null ? "—" : String(n);
}

export function kpiMarkdownCard(ids: SummaryEntityIds = {}, counts: SummaryCounts = {}): CardConfig {
  return {
    type: "markdown",
    text_only: true,
    content: [
      `🔴 **${kpiValue("overdue", ids, counts)}** overdue`,
      `⚡ **${kpiValue("triggered", ids, counts)}** triggered`,
      `🟡 **${kpiValue("due_soon", ids, counts)}** due soon`,
      `🟢 **${kpiValue("ok", ids, counts)}** ok`,
    ].join(" · "),
  };
}

// Onboarding card shown when there are zero maintenance objects. Pattern
// follows HA core's home-overview-view-strategy empty-state branch.
//
// Two buttons:
//   • "Open Maintenance panel" — plain navigate, the dependable path.
//   • "Add object" — fire-dom-event with a custom ll-custom payload that
//     our document-level handler (see registerLlCustomHandler below) catches
//     and turns into a deep-link navigation. This demonstrates the fire-dom
//     pattern HA core uses for in-place actions, without requiring us to
//     rip our object-add dialog out of the panel into a custom element.
function emptyStateView(): ViewConfig {
  return {
    title: "Maintenance",
    type: "panel",
    cards: [
      {
        type: "empty-state",
        icon: "mdi:wrench-clock",
        icon_color: "primary",
        content_only: true,
        title: "No maintenance objects yet",
        content:
          "Open the Maintenance panel to add your first object — pool pump, HVAC filter, vehicle, anything that needs scheduled care.",
        buttons: [
          {
            icon: "mdi:wrench",
            text: "Open Maintenance panel",
            appearance: "filled",
            variant: "brand",
            tap_action: {
              action: "navigate",
              navigation_path: "/maintenance-supporter",
            },
          },
          {
            icon: "mdi:plus",
            text: "Add object",
            appearance: "outlined",
            variant: "brand",
            tap_action: {
              action: "fire-dom-event",
              ll_custom: { type: "maintenance-supporter:add-object" },
            },
          },
        ],
      },
    ],
  };
}

function overviewView(summaryIds: SummaryEntityIds = {}, counts: SummaryCounts = {}): ViewConfig {
  const kpiCard = kpiMarkdownCard(summaryIds, counts);
  return {
    title: "Overview",
    icon: "mdi:wrench-clock",
    path: "overview",
    type: "sections",
    // Header runs above the main content — present on every screen size
    // so the user always sees the headline counts. Layout "responsive"
    // matches HA core's home strategy.
    header: { layout: "responsive", card: kpiCard },
    // Sidebar appears only on screens wide enough for ≥2 columns. Mirrors
    // the same KPI card so power users with a wide dashboard never lose
    // sight of it as they scroll long lists.
    sidebar: {
      sections: [
        {
          type: "grid",
          cards: [
            {
              type: "heading",
              heading: "Status",
              heading_style: "title",
            },
            kpiCard,
          ],
        },
      ],
      visibility: [LARGE_SCREEN_CONDITION],
      content_label: "Tasks",
      sidebar_label: "Status",
    },
    sections: [
      makeCardSection({
        type: "custom:maintenance-supporter-card",
        show_header: false,
        filter_status: ["overdue", "triggered", "due_soon"],
      }),
    ],
  };
}

function viewsByArea(
  objects: MaintenanceObjectResp[],
  areas: Record<string, AreaEntry>,
): ViewConfig[] {
  const byArea = new Map<string | null, MaintenanceObjectResp[]>();
  for (const obj of objects) {
    const aid = obj.object.area_id || null;
    if (!byArea.has(aid)) byArea.set(aid, []);
    byArea.get(aid)!.push(obj);
  }

  const areaIds = Array.from(byArea.keys()).filter(
    (a): a is string => a !== null,
  );
  areaIds.sort((a, b) => {
    const na = areas[a]?.name || a;
    const nb = areas[b]?.name || b;
    return na.localeCompare(nb);
  });

  const views: ViewConfig[] = [];
  for (const areaId of areaIds) {
    const objs = byArea.get(areaId)!;
    if (objs.length === 0) continue;
    const areaInfo = areas[areaId];
    views.push({
      title: areaInfo?.name || areaId,
      icon: areaInfo?.icon || "mdi:floor-plan",
      path: `area-${areaId}`,
      type: "sections",
      sections: [
        makeCardSection({
          type: "custom:maintenance-supporter-card",
          show_header: false,
          filter_objects: objs.map((o) => o.object.name),
        }),
      ],
    });
  }

  const unassigned = byArea.get(null);
  if (unassigned && unassigned.length > 0) {
    views.push({
      title: "Unassigned",
      icon: "mdi:help-circle-outline",
      path: "unassigned",
      type: "sections",
      sections: [
        makeCardSection({
          type: "custom:maintenance-supporter-card",
          show_header: false,
          filter_objects: unassigned.map((o) => o.object.name),
        }),
      ],
    });
  }

  return views;
}

function viewsByStatus(objects: MaintenanceObjectResp[]): ViewConfig[] {
  const present = new Set<string>();
  for (const obj of objects) {
    for (const task of obj.tasks || []) {
      if (task.status) present.add(task.status);
    }
  }

  const views: ViewConfig[] = [];
  for (const v of STATUS_VIEWS) {
    if (!present.has(v.status)) continue;
    views.push({
      title: v.title,
      icon: v.icon,
      path: v.path,
      type: "sections",
      sections: [
        makeCardSection({
          type: "custom:maintenance-supporter-card",
          show_header: false,
          filter_status: [v.status],
        }),
      ],
    });
  }
  return views;
}

function viewsByFloor(
  objects: MaintenanceObjectResp[],
  areas: Record<string, AreaEntry>,
  floors: Record<string, FloorEntry>,
): ViewConfig[] {
  // Build {floor_id → [object…]} via area.floor_id lookup; null bucket for
  // objects whose area has no floor OR whose object has no area at all.
  const byFloor = new Map<string | null, MaintenanceObjectResp[]>();
  for (const obj of objects) {
    const aid = obj.object.area_id;
    const fid = aid ? areas[aid]?.floor_id || null : null;
    if (!byFloor.has(fid)) byFloor.set(fid, []);
    byFloor.get(fid)!.push(obj);
  }

  // Floor sort: by `level` then name (matches HA's own floor sorting).
  const floorIds = Array.from(byFloor.keys()).filter(
    (f): f is string => f !== null,
  );
  floorIds.sort((a, b) => {
    const fa = floors[a];
    const fb = floors[b];
    const la = fa?.level ?? 0;
    const lb = fb?.level ?? 0;
    if (la !== lb) return la - lb;
    return (fa?.name || a).localeCompare(fb?.name || b);
  });

  const views: ViewConfig[] = [];
  for (const floorId of floorIds) {
    const objs = byFloor.get(floorId)!;
    if (objs.length === 0) continue;
    const floor = floors[floorId];
    views.push({
      title: floor?.name || floorId,
      icon: floor?.icon || "mdi:home-floor-1",
      path: `floor-${floorId}`,
      type: "sections",
      sections: [
        makeCardSection({
          type: "custom:maintenance-supporter-card",
          show_header: false,
          filter_objects: objs.map((o) => o.object.name),
        }),
      ],
    });
  }

  const unassigned = byFloor.get(null);
  if (unassigned && unassigned.length > 0) {
    views.push({
      title: "Other",
      icon: "mdi:help-circle-outline",
      path: "other",
      type: "sections",
      sections: [
        makeCardSection({
          type: "custom:maintenance-supporter-card",
          show_header: false,
          filter_objects: unassigned.map((o) => o.object.name),
        }),
      ],
    });
  }

  return views;
}

function viewsByCalendar(_objects: MaintenanceObjectResp[]): ViewConfig[] {
  // One view per window. Each is a panel-typed view with a single
  // ``maintenance-supporter-calendar`` card pinned to the matching
  // window_days. Window-chips inside the card are hidden because the
  // tab-bar already serves as the window selector.
  const WINDOWS: Array<{
    title: string;
    icon: string;
    path: string;
    window_days: 7 | 14 | 30 | 365;
  }> = [
    { title: "Week", icon: "mdi:calendar-week", path: "cal-7", window_days: 7 },
    { title: "Fortnight", icon: "mdi:calendar-week-begin", path: "cal-14", window_days: 14 },
    { title: "Month", icon: "mdi:calendar-month", path: "cal-30", window_days: 30 },
    { title: "Year", icon: "mdi:calendar-clock", path: "cal-365", window_days: 365 },
  ];

  return WINDOWS.map((w) => ({
    title: w.title,
    icon: w.icon,
    path: w.path,
    type: "panel",
    cards: [
      {
        type: "custom:maintenance-supporter-calendar-card",
        window_days: w.window_days,
        show_window_chips: false,
        show_user_filter: true,
      },
    ],
  }));
}

function viewsByDueDate(objects: MaintenanceObjectResp[]): ViewConfig[] {
  // Bucket-presence pre-check so we don't render an empty "Today" tab.
  const presentBuckets = new Set<number>();
  for (const obj of objects) {
    for (const task of obj.tasks || []) {
      const d = task.days_until_due;
      if (d === null || d === undefined) continue;
      DUE_DATE_VIEWS.forEach((v, i) => {
        if (v.matches(d)) presentBuckets.add(i);
      });
    }
  }

  const views: ViewConfig[] = [];
  DUE_DATE_VIEWS.forEach((v, i) => {
    if (!presentBuckets.has(i)) return;
    views.push({
      title: v.title,
      icon: v.icon,
      path: v.path,
      type: "sections",
      sections: [
        makeCardSection({
          type: "custom:maintenance-supporter-card",
          show_header: false,
          ...v.filter,
        }),
      ],
    });
  });
  return views;
}

export class MaintenanceDashboardStrategy extends HTMLElement {
  static getCreateSuggestions(_hass: HassLike) {
    return {
      title: "Maintenance Supporter",
      icon: "mdi:wrench-clock",
    };
  }

  // Lazy-load editor so picker users who never hit "Edit Dashboard" don't
  // pay for the LitElement bundle. HA's areas-dashboard-strategy uses the
  // same pattern.
  static async getConfigElement() {
    // The editor element is registered in the same bundle below — just
    // construct it. The dynamic import would work too, but with esbuild
    // bundling everything into one file there's nothing extra to load.
    return document.createElement(EDITOR_TAG);
  }

  static async generate(
    config: MaintenanceDashboardStrategyConfig | undefined,
    hass: HassLike,
  ): Promise<DashboardConfig> {
    if (hass.config?.state === STATE_NOT_RUNNING) {
      return {
        views: [
          { type: "sections", sections: [{ cards: [{ type: "starting" }] }] },
        ],
      };
    }
    if (hass.config?.recovery_mode) {
      return {
        views: [
          {
            type: "sections",
            sections: [{ cards: [{ type: "recovery-mode" }] }],
          },
        ],
      };
    }

    let response: { objects: MaintenanceObjectResp[] };
    try {
      response = await hass.connection.sendMessagePromise<{
        objects: MaintenanceObjectResp[];
      }>({ type: "maintenance_supporter/objects" });
    } catch {
      return {
        title: "Maintenance",
        views: [
          {
            title: "Maintenance",
            cards: [
              {
                type: "markdown",
                content:
                  "**Maintenance Supporter** is not loaded. Install/enable the integration first.",
              },
            ],
          },
        ],
      };
    }

    const objects = response.objects || [];

    // Onboarding short-circuit. With zero objects the area / status / floor /
    // due_date branches all produce just the Overview view (which shows
    // "no tasks") — that's a useless first impression. Show an actionable
    // empty-state instead so the user knows where to click next.
    if (objects.length === 0) {
      return { title: "Maintenance", views: [emptyStateView()] };
    }

    const groupBy: GroupBy = config?.group_by ?? "area";

    // Record-of-builders pattern from HA core's home-overview-view-strategy
    // — each entry is a no-arg lambda producing the per-mode views. Extending
    // with a new group_by mode means adding one entry instead of growing an
    // if/else chain.
    const viewBuilders: Record<GroupBy, () => ViewConfig[]> = {
      area: () => viewsByArea(objects, hass.areas || {}),
      status: () => viewsByStatus(objects),
      floor: () => viewsByFloor(objects, hass.areas || {}, hass.floors || {}),
      due_date: () => viewsByDueDate(objects),
      calendar: () => viewsByCalendar(objects),
    };

    // Registry-resolved summary-sensor ids + live counts (#86); tolerate older
    // backends. The counts are the fallback the chips render when the summary
    // sensors don't exist (orphan object entries after the global entry was
    // deleted), so they never read "unknown".
    let summaryIds: SummaryEntityIds = {};
    let counts: SummaryCounts = {};
    try {
      const stats = await hass.connection.sendMessagePromise<
        { summary_entity_ids?: SummaryEntityIds } & SummaryCounts
      >({
        type: "maintenance_supporter/statistics",
      });
      summaryIds = stats.summary_entity_ids || {};
      counts = {
        overdue: stats.overdue,
        triggered: stats.triggered,
        due_soon: stats.due_soon,
        ok: stats.ok,
      };
    } catch { /* fall back to the documented default ids */ }

    return {
      title: "Maintenance",
      views: [
        overviewView(summaryIds, counts),
        ...(viewBuilders[groupBy] ?? viewBuilders.area)(),
      ],
    };
  }
}

// ── Editor ──────────────────────────────────────────────────────────────────
//
// Minimal LovelaceStrategyEditor: a single dropdown for ``group_by``. Pattern
// follows HA core's ``hui-areas-dashboard-strategy-editor`` — LitElement that
// exposes setConfig(), holds the current config in @state, and dispatches
// "config-changed" with the new config on user input.
//
// We keep the editor as a plain HTMLElement (no Lit dependency) because the
// strategy file otherwise stays Lit-free. It's enough HTML for one <select>.

const GROUP_BY_OPTIONS: Array<{ value: GroupBy; label: string }> = [
  { value: "area", label: "By area (default)" },
  { value: "status", label: "By status (Overdue / Triggered / Due Soon / OK)" },
  { value: "floor", label: "By floor (uses HA floors)" },
  { value: "due_date", label: "By due date (Overdue / Today / Week / Month / Later)" },
  { value: "calendar", label: "Rolling calendar (Week / Fortnight / Month / Year)" },
];

class MaintenanceStrategyEditor extends HTMLElement {
  private _config: MaintenanceDashboardStrategyConfig = {
    type: "custom:maintenance-supporter",
  };
  private _hass: HassLike | undefined;

  set hass(hass: HassLike | undefined) {
    this._hass = hass;
  }

  setConfig(config: MaintenanceDashboardStrategyConfig): void {
    this._config = config;
    this._render();
  }

  connectedCallback(): void {
    this._render();
  }

  private _render(): void {
    const current = this._config.group_by ?? "area";
    const options = GROUP_BY_OPTIONS.map(
      (o) =>
        `<option value="${o.value}"${o.value === current ? " selected" : ""}>${o.label}</option>`,
    ).join("");
    this.innerHTML = `
      <style>
        :host, .editor { display: block; padding: 16px 0; }
        label { display: block; font-weight: 500; margin-bottom: 8px; }
        select {
          width: 100%; padding: 8px; font-size: 14px;
          background: var(--card-background-color, white);
          color: var(--primary-text-color, black);
          border: 1px solid var(--divider-color, #e0e0e0);
          border-radius: 4px;
        }
        .help {
          margin-top: 8px; font-size: 12px;
          color: var(--secondary-text-color, #666);
        }
      </style>
      <div class="editor">
        <label for="group-by">Group views by</label>
        <select id="group-by">${options}</select>
        <div class="help">
          The "Overview" view is always first. Empty groups are skipped.
        </div>
      </div>
    `;
    const select = this.querySelector("#group-by") as HTMLSelectElement | null;
    select?.addEventListener("change", () => {
      const newConfig: MaintenanceDashboardStrategyConfig = {
        ...this._config,
        group_by: select.value as GroupBy,
      };
      this._config = newConfig;
      this.dispatchEvent(
        new CustomEvent("config-changed", {
          detail: { config: newConfig },
          bubbles: true,
          composed: true,
        }),
      );
    });
  }
}

// ── Section Strategy ────────────────────────────────────────────────────────
//
// A standalone section users can drop into ANY view (HA's home dashboard, the
// areas dashboard's per-area view, a custom dashboard) to surface a slice of
// maintenance tasks in context. Mirrors HA core's ``common-controls`` section
// strategy pattern. YAML usage:
//
//   sections:
//     - strategy:
//         type: custom:maintenance-supporter-section
//         area_id: kitchen        # optional — restrict to one area
//         filter_status:          # optional — restrict to certain statuses
//           - overdue
//           - triggered
//         title: Kitchen upkeep   # optional heading card
//         max_items: 5            # optional row limit
//
// At least one filter is recommended; with no filters it shows everything,
// which is what the dashboard strategy already does.

const SECTION_STRATEGY_TYPE = "maintenance-supporter-section";
const SECTION_STRATEGY_TAG = `ll-strategy-section-${SECTION_STRATEGY_TYPE}`;

interface MaintenanceSectionStrategyConfig {
  type: "custom:maintenance-supporter-section" | "maintenance-supporter-section";
  area_id?: string;
  filter_status?: string[];
  filter_objects?: string[];
  filter_due_min_days?: number;
  filter_due_max_days?: number;
  title?: string;
  max_items?: number;
}

class MaintenanceSectionStrategy extends HTMLElement {
  static async generate(
    config: MaintenanceSectionStrategyConfig | undefined,
    hass: HassLike,
  ): Promise<SectionConfig> {
    const card: CardConfig = {
      type: "custom:maintenance-supporter-card",
      show_header: false,
    };

    // Pass through optional filters
    if (config?.filter_status?.length) card.filter_status = config.filter_status;
    if (config?.max_items) card.max_items = config.max_items;
    if (config?.filter_due_min_days !== undefined) {
      card.filter_due_min_days = config.filter_due_min_days;
    }
    if (config?.filter_due_max_days !== undefined) {
      card.filter_due_max_days = config.filter_due_max_days;
    }

    // Resolve area_id → object names via WS (the card filters by name).
    // Falls back gracefully if the WS call fails — empty filter means
    // "show everything", which still renders something useful.
    let names: string[] | undefined = config?.filter_objects;
    if (config?.area_id && !names) {
      try {
        const r = await hass.connection.sendMessagePromise<{
          objects: MaintenanceObjectResp[];
        }>({ type: "maintenance_supporter/objects" });
        names = (r.objects || [])
          .filter((o) => o.object.area_id === config.area_id)
          .map((o) => o.object.name);
      } catch {
        // ignore — card will show all
      }
    }
    if (names && names.length > 0) card.filter_objects = names;

    const cards: CardConfig[] = [];
    if (config?.title) {
      cards.push({
        type: "heading",
        heading: config.title,
        heading_style: "title",
      });
    }
    cards.push(card);

    return { type: "grid", cards };
  }
}

// v2.3.5 (issue #52 follow-up): wrap registration in try/catch + emit a
// banner-style console.log so that when ANY user hits a strategy timeout
// we can see in the browser console exactly which step failed instead of
// guessing. The non-error console.log also serves as a positive load
// signal — if the user doesn't see "[maintenance-supporter] strategy
// bundle loaded" at all, the script never ran (cached index.html / CSP /
// MIME-type problem). If they see the log but still get the timeout,
// something else (Lovelace resource conflict, HACS plugin clash) is
// holding the registry.
const STRATEGY_VERSION = "2.3.6";
const LOG_PREFIX = "[maintenance-supporter]";

function safeDefine(tag: string, ctor: CustomElementConstructor): "ok" | "skipped" | string {
  try {
    if (customElements.get(tag)) {
      return "skipped";
    }
    customElements.define(tag, ctor);
    return "ok";
  } catch (err) {
    return `ERROR: ${String(err)}`;
  }
}

// NOTE: the dashboard tag (STRATEGY_TAG) is intentionally NOT defined here.
// It is owned by the tiny zero-import shim (maintenance-strategy-shim.ts),
// which is what HA loads via frontend_extra_module_url so registration wins
// HA's 5 s whenDefined race even under heavy plugin load. This heavy bundle is
// lazy-loaded by the shim on first generate()/getConfigElement(); the shim
// calls our exported MaintenanceDashboardStrategy.generate(). We still define
// the editor + section strategies here (not on HA's timeout-critical path).
const _defineResults = {
  editor: safeDefine(EDITOR_TAG, MaintenanceStrategyEditor),
  section: safeDefine(SECTION_STRATEGY_TAG, MaintenanceSectionStrategy),
};

console.log(
  `${LOG_PREFIX} strategy bundle v${STRATEGY_VERSION} loaded — registrations:`,
  _defineResults,
  "— run maintenanceSupporterDiagnose() in console for full state",
);

// Global devtools helper — typing maintenanceSupporterDiagnose() in the
// browser console returns a snapshot of registration state + module URLs
// + Lovelace resources. Easier than asking users to paste a multi-line
// snippet they then can't run for whatever reason.
(window as unknown as { maintenanceSupporterDiagnose?: () => Promise<unknown> }).maintenanceSupporterDiagnose =
  async () => {
    const tag = STRATEGY_TAG;
    let resources: Array<{ url?: string }> = [];
    try {
      const ha = document.querySelector("home-assistant") as
        | (HTMLElement & { hass?: { connection: { sendMessagePromise<T>(m: Record<string, unknown>): Promise<T> } } })
        | null;
      if (ha?.hass) {
        const r = await ha.hass.connection.sendMessagePromise<{ resources?: Array<{ url?: string }> }>(
          { type: "lovelace/resources" },
        );
        resources = r.resources || [];
      }
    } catch (e) { resources = [{ url: `<error: ${String(e)}>` }]; }
    return {
      version: STRATEGY_VERSION,
      el_registered: !!customElements.get(tag),
      el_class_excerpt: customElements.get(tag)?.toString().slice(0, 100) || null,
      customStrategies_dashboard: (window as unknown as { customStrategies?: Array<{ type: string; strategyType: string }> })
        .customStrategies?.filter((s) => s.strategyType === "dashboard").map((s) => s.type) || [],
      scriptTagsInPage: Array.from(document.querySelectorAll("script[type=module]"))
        .map((s) => (s as HTMLScriptElement).src)
        .filter((s) => /maint|strateg/i.test(s)),
      lovelaceResources_matching: resources.filter((r) => /maint|strateg/i.test(r.url || "")).map((r) => r.url),
      registrations: _defineResults,
    };
  };

// ── Phase 5: Status Section-Strategies ──────────────────────────────────────
//
// Three small status sections so users with a Lovelace-only setup
// can monitor vacation / budget / groups state without leaving the dashboard.
// v2.4.0 update: each strategy now delegates to a live interactive custom
// card (vacation-section-card / budget-section-card / groups-section-card)
// that lets admins edit values inline.
//
// v2.3.4 (issue #52): the section-card modules are LAZY-loaded inside
// generate() — bundling them at module top blew the strategy file up to
// 509 KB, and HA's 5 s strategy-load timeout was firing on slow connections
// before customElements.define for ll-strategy-dashboard-* could run. Now
// the strategy bundle is tiny (~10 KB) and registers instantly; the heavy
// section-card code only loads when a section actually renders.

class MaintenanceVacationSectionStrategy extends HTMLElement {
  static async generate(
    _config: { type: string; title?: string },
    _hass: HassLike,
  ): Promise<SectionConfig> {
    await import("./components/vacation-section-card");
    return {
      type: "grid",
      cards: [
        {
          type: "custom:maintenance-vacation-section-card",
          title: _config?.title,
        },
      ],
    };
  }
}

class MaintenanceBudgetSectionStrategy extends HTMLElement {
  static async generate(
    _config: { type: string; title?: string },
    _hass: HassLike,
  ): Promise<SectionConfig> {
    await import("./components/budget-section-card");
    return {
      type: "grid",
      cards: [
        {
          type: "custom:maintenance-budget-section-card",
          title: _config?.title,
        },
      ],
    };
  }
}

class MaintenanceGroupsSectionStrategy extends HTMLElement {
  static async generate(
    _config: { type: string; title?: string },
    _hass: HassLike,
  ): Promise<SectionConfig> {
    await import("./components/groups-section-card");
    return {
      type: "grid",
      cards: [
        {
          type: "custom:maintenance-groups-section-card",
          title: _config?.title,
        },
      ],
    };
  }
}

// Register the three section strategies
const VACATION_TAG = "ll-strategy-section-maintenance-supporter-vacation";
const BUDGET_TAG = "ll-strategy-section-maintenance-supporter-budget";
const GROUPS_TAG = "ll-strategy-section-maintenance-supporter-groups";
if (!customElements.get(VACATION_TAG)) customElements.define(VACATION_TAG, MaintenanceVacationSectionStrategy);
if (!customElements.get(BUDGET_TAG)) customElements.define(BUDGET_TAG, MaintenanceBudgetSectionStrategy);
if (!customElements.get(GROUPS_TAG)) customElements.define(GROUPS_TAG, MaintenanceGroupsSectionStrategy);

// ── fire-dom-event handler ──────────────────────────────────────────────────
//
// HA's standard ``tap_action: { action: "fire-dom-event", ll_custom: {...} }``
// dispatches an ``ll-custom`` CustomEvent on the tapped element. We register
// one bubble-phase listener at document level that handles every payload
// whose ``type`` is namespaced with ``maintenance-supporter:`` — currently:
//
//   maintenance-supporter:add-object  → in-place open of MaintenanceObjectDialog
//   maintenance-supporter:open-task    → in-place open of MaintenanceTaskDialog
//                                        (with entry_id + task_id in detail)
//
// The dialog components are imported via dialog-mount.ts and mounted lazily
// onto document.body the first time they are needed. If the in-place mount
// fails (e.g. <home-assistant> not yet ready), we fall back to deep-linking
// the panel via ?ms_action=… so the user still ends up where they want.

// v2.3.4 (issue #52): dialog-mount is heavy (pulls in 7 dialog components +
// their Lit runtime). We only need it when the user actually triggers a
// fire-dom-event from a strategy-generated card, so the import is dynamic
// and runs the first time a maintenance-supporter:* event fires.
type DialogMountModule = typeof import("./dialog-mount");
let _dialogMountPromise: Promise<DialogMountModule> | null = null;
function getDialogMount(): Promise<DialogMountModule> {
  if (!_dialogMountPromise) {
    // Don't cache a REJECTED import: after a version bump the browser may hold a
    // stale strategy bundle whose hashed dialog-mount chunk 404s once; caching
    // that rejection would dead-end every dialog action until a full reload.
    // Clear the cache on failure so a later attempt (or fixed chunk) can retry.
    _dialogMountPromise = import("./dialog-mount").catch((err) => {
      _dialogMountPromise = null;
      throw err;
    });
  }
  return _dialogMountPromise;
}

interface LlCustomEventDetail {
  type?: string;
  entry_id?: unknown;
  task_id?: unknown;
  [key: string]: unknown;
}

const LL_CUSTOM_HANDLER_FLAG = "_msSupporterLlCustomBound";

function deepLink(path: string): void {
  history.pushState(null, "", path);
  window.dispatchEvent(new CustomEvent("location-changed"));
}

function registerLlCustomHandler(): void {
  const w = window as unknown as Record<string, unknown>;
  if (w[LL_CUSTOM_HANDLER_FLAG]) return; // idempotent — strategy file may load twice
  w[LL_CUSTOM_HANDLER_FLAG] = true;

  document.addEventListener("ll-custom", (event: Event) => {
    const raw = (event as CustomEvent<LlCustomEventDetail>).detail;
    if (!raw || typeof raw !== "object") return;
    // HA's `tap_action: { action: "fire-dom-event", ll_custom: {…} }` dispatches
    // `ll-custom` with the ENTIRE action config as `detail`, nesting our payload
    // under `.ll_custom` (verified live on HA 2026.6 — issue #69: "Add object"
    // did nothing because we read `detail.type`, which was undefined). Direct
    // dispatchers (the calendar card, the panel) put the payload at the top
    // level. Accept both: prefer the nested `ll_custom` object when present.
    const nested = (raw as { ll_custom?: unknown }).ll_custom;
    const detail = (
      nested && typeof nested === "object" ? nested : raw
    ) as LlCustomEventDetail;
    if (!detail || typeof detail.type !== "string") return;
    if (!detail.type.startsWith("maintenance-supporter:")) return;
    const action = detail.type.slice("maintenance-supporter:".length);

    if (action === "deep-link" && typeof detail.path === "string") {
      deepLink(detail.path);
      return;
    }

    // Everything else needs the heavy dialog-mount module — load it lazily.
    void (async () => {
      let dm: DialogMountModule;
      try {
        dm = await getDialogMount();
      } catch {
        // dialog-mount chunk unavailable (e.g. stale bundle after an update) —
        // degrade to opening the panel instead of a silent, unhandled failure.
        deepLink("/maintenance-supporter");
        return;
      }

      if (action === "add-object") {
        if (dm.openCreateObjectDialog()) return;
        deepLink("/maintenance-supporter?ms_action=add_object");
        return;
      }

      if (
        action === "open-task" &&
        typeof detail.entry_id === "string" &&
        typeof detail.task_id === "string"
      ) {
        if (dm.openTaskQuickActions(detail.entry_id, detail.task_id)) return;
        if (dm.openEditTaskDialog(detail.entry_id, detail.task_id)) return;
        deepLink(
          `/maintenance-supporter?entry_id=${encodeURIComponent(detail.entry_id)}` +
            `&task_id=${encodeURIComponent(detail.task_id)}`,
        );
        return;
      }

      if (action === "open-object" && typeof detail.entry_id === "string") {
        if (dm.openObjectQuickActions(detail.entry_id)) return;
        deepLink(`/maintenance-supporter?entry_id=${encodeURIComponent(detail.entry_id)}`);
        return;
      }

      if (
        action === "edit-history" &&
        typeof detail.entry_id === "string" &&
        typeof detail.task_id === "string" &&
        typeof detail.original_timestamp === "string"
      ) {
        const haRoot = document.querySelector(
          "home-assistant",
        ) as (HTMLElement & { hass?: { connection: { sendMessagePromise<T>(m: Record<string, unknown>): Promise<T> } } }) | null;
        const hass = haRoot?.hass;
        if (!hass) return;
        try {
          const r = await hass.connection.sendMessagePromise<{
            tasks?: Array<{ id?: string; history?: Array<Record<string, unknown>> }>;
          }>({
            type: "maintenance_supporter/object",
            entry_id: detail.entry_id,
          });
          const task = r.tasks?.find((t) => t.id === detail.task_id);
          const histEntry = task?.history?.find(
            (h) => h.timestamp === detail.original_timestamp,
          );
          if (!histEntry) return;
          dm.openHistoryEditDialog({
            entry_id: detail.entry_id as string,
            task_id: detail.task_id as string,
            original_timestamp: detail.original_timestamp as string,
            type: (histEntry.type as string) || "completed",
            timestamp: (histEntry.timestamp as string) || (detail.original_timestamp as string),
            notes: (histEntry.notes as string) ?? null,
            cost: (histEntry.cost as number | undefined) ?? null,
            duration: (histEntry.duration as number | undefined) ?? null,
            completed_by: (histEntry.completed_by as string) ?? null,
            used_parts: (histEntry.used_parts as Array<{ part_id: string; name?: string; quantity: number; entry_id?: string }> | null) ?? null,
          });
        } catch {
          deepLink("/maintenance-supporter");
        }
      }
    })();
  });
}

registerLlCustomHandler();

// Discovery — picked up by HA 2026.5+. Older HA ignores it silently.
const w = window as unknown as {
  customStrategies?: Array<{
    type: string;
    strategyType: string;
    name: string;
    description?: string;
    documentationURL?: string;
  }>;
};
w.customStrategies = w.customStrategies || [];

function registerStrategy(entry: {
  type: string;
  strategyType: string;
  name: string;
  description?: string;
  documentationURL?: string;
}): void {
  const exists = w.customStrategies!.some(
    (s) => s.type === entry.type && s.strategyType === entry.strategyType,
  );
  if (!exists) w.customStrategies!.push(entry);
}

registerStrategy({
  type: STRATEGY_TYPE,
  strategyType: "dashboard",
  name: "Maintenance Supporter",
  description:
    "Auto-generated dashboard. Group views by area, status, floor, or due date — picked from the strategy editor or YAML.",
  documentationURL:
    "https://github.com/iluebbe/maintenance_supporter#dashboard-strategy",
});

registerStrategy({
  type: SECTION_STRATEGY_TYPE,
  strategyType: "section",
  name: "Maintenance Supporter — Section",
  description:
    "Embed maintenance tasks (filterable by area, status, due date) as a section in any dashboard view.",
  documentationURL:
    "https://github.com/iluebbe/maintenance_supporter#section-strategy",
});

// Phase 5 status sections
registerStrategy({
  type: "maintenance-supporter-vacation",
  strategyType: "section",
  name: "Maintenance Supporter — Vacation Status",
  description: "Compact vacation-mode status banner. Tap to open settings in the panel.",
});
registerStrategy({
  type: "maintenance-supporter-budget",
  strategyType: "section",
  name: "Maintenance Supporter — Budget Status",
  description: "Monthly + yearly maintenance budget overview. Tap for details.",
});
registerStrategy({
  type: "maintenance-supporter-groups",
  strategyType: "section",
  name: "Maintenance Supporter — Groups",
  description: "List of configured task groups with member counts.",
});

export {};
