/**
 * Persistence of ticket filters across restarts / reopens (ticket #29).
 *
 * Frontend-only: filters are UI preferences, so they live behind the thin
 * {@link UiPreferencesGateway} (localStorage adapter) — the backend stays the
 * source of tickets, never of these preferences. Persistence is scoped **per
 * project and per surface** through two disjoint keys so the main list and the
 * {@link TicketPicker} popup never pollute each other:
 *
 *   - `tickets:listFilters:<projectId>`   — the main tickets window.
 *   - `tickets:pickerFilters:<projectId>` — the reusable ticket picker.
 *
 * We only ever store the *stable* criteria (`text`, `statuses`, `priorities`,
 * `sort`, `limit`, and — main list only — `assignedAgentId`). The opaque
 * `cursor` is **never** persisted: it is a transient pagination token whose reuse
 * across sessions is meaningless (G3-bis). Reads are defensive — unknown values
 * are dropped and a corrupt blob yields the default empty query.
 */

import type { TicketPriority, TicketStatus } from "@/domain";
import type {
  TicketListQuery,
  TicketListSort,
  TicketListSortDirection,
  TicketListSortField,
  UiPreferencesGateway,
} from "@/ports";

const VALID_STATUSES: readonly TicketStatus[] = [
  "open",
  "inProgress",
  "QA",
  "closed",
];
const VALID_PRIORITIES: readonly TicketPriority[] = [
  "low",
  "medium",
  "high",
  "critical",
];
const VALID_SORT_FIELDS: readonly TicketListSortField[] = [
  "number",
  "priority",
  "status",
  "title",
];
const VALID_SORT_DIRECTIONS: readonly TicketListSortDirection[] = ["asc", "desc"];

/** Storage key for the main tickets window, scoped to `projectId`. */
export function listFiltersKey(projectId: string): string {
  return `tickets:listFilters:${projectId}`;
}

/** Storage key for the {@link TicketPicker} popup, scoped to `projectId`. */
export function pickerFiltersKey(projectId: string): string {
  return `tickets:pickerFilters:${projectId}`;
}

function isObject(v: unknown): v is Record<string, unknown> {
  return typeof v === "object" && v !== null && !Array.isArray(v);
}

/** Keeps only the members of `allowed`, de-duplicated, order preserved. */
function filterEnumArray<T extends string>(
  raw: unknown,
  allowed: readonly T[],
): T[] | undefined {
  if (!Array.isArray(raw)) return undefined;
  const set = new Set(allowed);
  const out: T[] = [];
  for (const v of raw) {
    if (typeof v === "string" && set.has(v as T) && !out.includes(v as T)) {
      out.push(v as T);
    }
  }
  return out.length > 0 ? out : undefined;
}

function parseSort(raw: unknown): TicketListSort | undefined {
  if (!isObject(raw)) return undefined;
  const { field, direction } = raw;
  if (
    typeof field === "string" &&
    (VALID_SORT_FIELDS as readonly string[]).includes(field) &&
    typeof direction === "string" &&
    (VALID_SORT_DIRECTIONS as readonly string[]).includes(direction)
  ) {
    return {
      field: field as TicketListSortField,
      direction: direction as TicketListSortDirection,
    };
  }
  return undefined;
}

/** Options for {@link parseTicketFilters}. */
export interface ParseTicketFiltersOptions {
  /**
   * When provided, a persisted `assignedAgentId` is kept only if it belongs to
   * this set — an obsolete assignee (agent since deleted) is dropped. When
   * `undefined`, the assignee is validated for *shape* only (the caller does not
   * know the agent roster yet and reconciles later).
   */
  validAgentIds?: ReadonlySet<string>;
}

/**
 * Validates an untrusted persisted blob into a clean {@link TicketListQuery}.
 * Unknown fields/values are ignored; a non-object (corrupt JSON already yielded
 * `null` upstream) yields the default empty query. `cursor` is never read back.
 */
export function parseTicketFilters(
  raw: unknown,
  options: ParseTicketFiltersOptions = {},
): TicketListQuery {
  if (!isObject(raw)) return {};
  const query: TicketListQuery = {};

  if (typeof raw.text === "string" && raw.text.length > 0) {
    query.text = raw.text;
  }

  const statuses = filterEnumArray(raw.statuses, VALID_STATUSES);
  if (statuses) query.statuses = statuses;

  const priorities = filterEnumArray(raw.priorities, VALID_PRIORITIES);
  if (priorities) query.priorities = priorities;

  if (typeof raw.assignedAgentId === "string" && raw.assignedAgentId) {
    const { validAgentIds } = options;
    if (!validAgentIds || validAgentIds.has(raw.assignedAgentId)) {
      query.assignedAgentId = raw.assignedAgentId;
    }
  }

  const sort = parseSort(raw.sort);
  if (sort) query.sort = sort;

  if (
    typeof raw.limit === "number" &&
    Number.isInteger(raw.limit) &&
    raw.limit > 0
  ) {
    query.limit = raw.limit;
  }

  // `cursor` is intentionally never restored.
  return query;
}

/**
 * Reduces a live query to the persistable subset for the **main list**: keeps
 * `text/statuses/priorities/assignedAgentId/sort/limit`, drops `cursor`. Returns
 * `null` when nothing is worth storing (so the caller can remove the key).
 */
export function sanitizeListFilters(query: TicketListQuery): TicketListQuery | null {
  const out: TicketListQuery = {};
  if (query.text) out.text = query.text;
  if (query.statuses && query.statuses.length > 0) out.statuses = query.statuses;
  if (query.priorities && query.priorities.length > 0) {
    out.priorities = query.priorities;
  }
  if (query.assignedAgentId) out.assignedAgentId = query.assignedAgentId;
  if (query.sort) out.sort = query.sort;
  if (typeof query.limit === "number") out.limit = query.limit;
  return Object.keys(out).length > 0 ? out : null;
}

/**
 * Reduces a live query to the persistable subset for the **picker**: like the
 * list but *without* `assignedAgentId` (assignee is a main-list-only preference)
 * and without `limit` (the picker's limit is a caller/business concern, not a
 * user preference). `cursor` is likewise dropped.
 */
export function sanitizePickerFilters(
  query: TicketListQuery,
): TicketListQuery | null {
  const out: TicketListQuery = {};
  if (query.text) out.text = query.text;
  if (query.statuses && query.statuses.length > 0) out.statuses = query.statuses;
  if (query.priorities && query.priorities.length > 0) {
    out.priorities = query.priorities;
  }
  if (query.sort) out.sort = query.sort;
  return Object.keys(out).length > 0 ? out : null;
}

/** Reads + validates the persisted main-list filters (empty query on miss). */
export function hydrateListFilters(
  prefs: UiPreferencesGateway | undefined,
  projectId: string,
  options?: ParseTicketFiltersOptions,
): TicketListQuery {
  if (!prefs) return {};
  return parseTicketFilters(prefs.read(listFiltersKey(projectId)), options);
}

/** Persists (or clears) the main-list filters for a project. */
export function persistListFilters(
  prefs: UiPreferencesGateway | undefined,
  projectId: string,
  query: TicketListQuery,
): void {
  if (!prefs) return;
  const sanitized = sanitizeListFilters(query);
  const key = listFiltersKey(projectId);
  if (sanitized) prefs.write(key, sanitized);
  else prefs.remove(key);
}

/** Reads + validates the persisted picker filters (empty query on miss). */
export function hydratePickerFilters(
  prefs: UiPreferencesGateway | undefined,
  projectId: string,
): TicketListQuery {
  if (!prefs) return {};
  return parseTicketFilters(prefs.read(pickerFiltersKey(projectId)));
}

/** Persists (or clears) the picker filters for a project. */
export function persistPickerFilters(
  prefs: UiPreferencesGateway | undefined,
  projectId: string,
  query: TicketListQuery,
): void {
  if (!prefs) return;
  const sanitized = sanitizePickerFilters(query);
  const key = pickerFiltersKey(projectId);
  if (sanitized) prefs.write(key, sanitized);
  else prefs.remove(key);
}
