import { useCallback, useMemo } from "react";
import { useSearchParams } from "react-router-dom";
import type { DashboardPeriod } from "../shared/types";
import { computeDateRange } from "../utils/dateRange";

const VALID_PERIODS: readonly DashboardPeriod[] = [
  "month",
  "3months",
  "6months",
  "year",
  "12months",
  "all",
  "custom",
];

function isValidPeriod(p: string | null): p is DashboardPeriod {
  return p !== null && (VALID_PERIODS as readonly string[]).includes(p);
}

function isValidIsoDate(s: string | null): s is string {
  return !!s && /^\d{4}-\d{2}-\d{2}$/.test(s);
}

function currentYearRange(today: Date = new Date()): { from: string; to: string } {
  const year = today.getFullYear();
  return { from: `${year}-01-01`, to: `${year}-12-31` };
}

// Matches a bare (optionally negative) integer token, e.g. "3" or "-3", but
// not "3.5", "abc", "" or "1e3" — deliberately stricter than `Number(...)` so
// that hex/exponential-notation strings from a hand-edited URL don't sneak
// through as valid ids.
const INTEGER_TOKEN = /^-?\d+$/;

/**
 * Pure parser for the `sources` query param (comma-separated account/import
 * source ids), exposed for the same testability reason as
 * `resolveReportsPeriod`. Invalid tokens (non-numeric, decimal, empty from a
 * stray comma, or too large to be a safe integer) are dropped individually
 * rather than invalidating the whole list — a single corrupted token in a
 * bookmarked/hand-edited URL should not silently discard the rest of an
 * otherwise valid selection. Missing param or no valid token → `[]`, which
 * means "no filter" (all accounts).
 */
export function parseAccountIds(raw: string | null): number[] {
  if (!raw) return [];
  return raw
    .split(",")
    .map((token) => token.trim())
    .filter((token) => INTEGER_TOKEN.test(token))
    .map(Number)
    .filter((n) => Number.isSafeInteger(n));
}

/**
 * Inverse of `parseAccountIds`: serializes account ids back to the `sources`
 * query param value, or `null` when the param should be removed from the URL
 * entirely (empty/all-invalid selection = no filter — mirrors how `from`/`to`
 * are omitted for a non-custom period).
 */
export function serializeAccountIds(accountIds: number[]): string | null {
  const valid = accountIds.filter((n) => Number.isSafeInteger(n));
  return valid.length > 0 ? valid.join(",") : null;
}

/**
 * Pure resolver used by the hook and unit tests. Exposed to keep the core
 * logic hookless and testable without rendering a router.
 */
export function resolveReportsPeriod(
  rawFrom: string | null,
  rawTo: string | null,
  rawPeriod: string | null,
  today: Date = new Date(),
): { from: string; to: string; period: DashboardPeriod } {
  if (isValidIsoDate(rawFrom) && isValidIsoDate(rawTo)) {
    const p = isValidPeriod(rawPeriod) ? rawPeriod : "custom";
    return { from: rawFrom, to: rawTo, period: p };
  }
  if (isValidPeriod(rawPeriod) && rawPeriod !== "custom") {
    const range = computeDateRange(rawPeriod);
    const { from: defaultFrom, to: defaultTo } = currentYearRange(today);
    return {
      from: range.dateFrom ?? defaultFrom,
      to: range.dateTo ?? defaultTo,
      period: rawPeriod,
    };
  }
  const { from, to } = currentYearRange(today);
  return { from, to, period: "custom" };
}

export interface UseReportsPeriodResult {
  from: string;
  to: string;
  period: DashboardPeriod;
  accountIds: number[];
  setPeriod: (period: DashboardPeriod) => void;
  setCustomDates: (from: string, to: string) => void;
  setAccountIds: (accountIds: number[]) => void;
}

/**
 * Reads/writes the active reporting period — and, additively, the account
 * (import source) filter — via the URL query string so both are bookmarkable
 * and shared across the report sub-routes.
 *
 * Defaults to the current civil year (Jan 1 → Dec 31) with no account filter
 * (`accountIds: []`, meaning all accounts).
 */
export function useReportsPeriod(): UseReportsPeriodResult {
  const [searchParams, setSearchParams] = useSearchParams();

  const rawPeriod = searchParams.get("period");
  const rawFrom = searchParams.get("from");
  const rawTo = searchParams.get("to");
  const rawSources = searchParams.get("sources");

  const { from, to, period } = useMemo(
    () => resolveReportsPeriod(rawFrom, rawTo, rawPeriod),
    [rawPeriod, rawFrom, rawTo],
  );

  const accountIds = useMemo(() => parseAccountIds(rawSources), [rawSources]);

  const setPeriod = useCallback(
    (next: DashboardPeriod) => {
      setSearchParams(
        (prev) => {
          const params = new URLSearchParams(prev);
          if (next === "custom") {
            params.set("period", "custom");
          } else {
            params.set("period", next);
            params.delete("from");
            params.delete("to");
          }
          return params;
        },
        { replace: true },
      );
    },
    [setSearchParams],
  );

  const setCustomDates = useCallback(
    (nextFrom: string, nextTo: string) => {
      setSearchParams(
        (prev) => {
          const params = new URLSearchParams(prev);
          params.set("period", "custom");
          params.set("from", nextFrom);
          params.set("to", nextTo);
          return params;
        },
        { replace: true },
      );
    },
    [setSearchParams],
  );

  const setAccountIds = useCallback(
    (nextAccountIds: number[]) => {
      setSearchParams(
        (prev) => {
          const params = new URLSearchParams(prev);
          const serialized = serializeAccountIds(nextAccountIds);
          if (serialized === null) {
            params.delete("sources");
          } else {
            params.set("sources", serialized);
          }
          return params;
        },
        { replace: true },
      );
    },
    [setSearchParams],
  );

  return { from, to, period, accountIds, setPeriod, setCustomDates, setAccountIds };
}
