import { execFileSync } from 'node:child_process';

export interface HyprlandPlacementClient {
  address?: string;
  at?: [number, number];
  floating?: boolean;
  hidden?: boolean;
  initialTitle?: string;
  mapped?: boolean;
  pid?: number;
  pinned?: boolean;
  size?: [number, number];
  title?: string;
}

export interface HyprlandPlacementBounds {
  x: number;
  y: number;
  width: number;
  height: number;
}

export interface HyprlandPlacementDispatchOptions {
  configProvider?: HyprlandConfigProvider;
  promote?: boolean;
}

type ExecFileSync = typeof execFileSync;
export type HyprlandConfigProvider = 'hyprlang' | 'lua';

export function shouldAttemptHyprlandWindowPlacement(
  platform: NodeJS.Platform = process.platform,
  env: NodeJS.ProcessEnv = process.env,
): boolean {
  return platform === 'linux' && Boolean(env.HYPRLAND_INSTANCE_SIGNATURE);
}

function parseHyprlandClients(output: string): HyprlandPlacementClient[] {
  const payloadStart = output.indexOf('[');
  if (payloadStart < 0) {
    return [];
  }

  const parsed = JSON.parse(output.slice(payloadStart)) as unknown;
  return Array.isArray(parsed) ? (parsed as HyprlandPlacementClient[]) : [];
}

function readHyprlandPlacementClients(run: ExecFileSync): HyprlandPlacementClient[] {
  return parseHyprlandClients(String(run('hyprctl', ['-j', 'clients'], { encoding: 'utf-8' })));
}

export function findHyprlandWindowForPlacement(
  clients: HyprlandPlacementClient[],
  options: {
    pid: number;
    title: string;
  },
): HyprlandPlacementClient | null {
  const title = options.title.trim();
  if (!title) {
    return null;
  }

  return (
    clients.find(
      (client) =>
        client.pid === options.pid &&
        client.address &&
        client.mapped !== false &&
        client.hidden !== true &&
        (client.title === title || client.initialTitle === title),
    ) ?? null
  );
}

export function buildHyprlandPlacementDispatches(
  client: HyprlandPlacementClient,
  bounds?: HyprlandPlacementBounds | null,
  options: HyprlandPlacementDispatchOptions = {},
): string[][] {
  if (!client.address) {
    return [];
  }

  const windowAddress = `address:${client.address}`;
  const configProvider = options.configProvider ?? 'hyprlang';
  const dispatches: string[][] = [];
  if (client.floating !== true) {
    dispatches.push(
      configProvider === 'lua'
        ? luaWindowDispatch('float', windowAddress, ['action = "on"'])
        : ['dispatch', 'setfloating', windowAddress],
    );
  }
  if (client.pinned === true) {
    dispatches.push(
      configProvider === 'lua'
        ? luaWindowDispatch('pin', windowAddress, ['action = "off"'])
        : ['dispatch', 'pin', windowAddress],
    );
  }
  const roundedBounds = roundPlacementBounds(bounds);
  if (roundedBounds) {
    if (configProvider === 'lua') {
      dispatches.push(
        luaWindowDispatch('resize', windowAddress, [
          `x = ${roundedBounds.width}`,
          `y = ${roundedBounds.height}`,
        ]),
      );
      dispatches.push(
        luaWindowDispatch('move', windowAddress, [
          `x = ${roundedBounds.x}`,
          `y = ${roundedBounds.y}`,
        ]),
      );
      dispatches.push(luaWindowSetProp(windowAddress, 'rounding', '0'));
      dispatches.push(luaWindowSetProp(windowAddress, 'border_size', '0'));
      dispatches.push(luaWindowSetProp(windowAddress, 'no_shadow', '1'));
      dispatches.push(luaWindowSetProp(windowAddress, 'no_blur', '1'));
      dispatches.push(luaWindowSetProp(windowAddress, 'decorate', '0'));
    } else {
      dispatches.push([
        'dispatch',
        'resizewindowpixel',
        `exact ${roundedBounds.width} ${roundedBounds.height},${windowAddress}`,
      ]);
      dispatches.push([
        'dispatch',
        'movewindowpixel',
        `exact ${roundedBounds.x} ${roundedBounds.y},${windowAddress}`,
      ]);
      dispatches.push(['dispatch', 'setprop', `${windowAddress} rounding 0`]);
      dispatches.push(['dispatch', 'setprop', `${windowAddress} border_size 0`]);
      dispatches.push(['dispatch', 'setprop', `${windowAddress} no_shadow 1`]);
      dispatches.push(['dispatch', 'setprop', `${windowAddress} no_blur 1`]);
      dispatches.push(['dispatch', 'setprop', `${windowAddress} decorate 0`]);
    }
  }
  if (options.promote !== false) {
    dispatches.push(
      configProvider === 'lua'
        ? luaWindowDispatch('alter_zorder', windowAddress, ['mode = "top"'])
        : ['dispatch', 'alterzorder', `top,${windowAddress}`],
    );
  }
  return dispatches;
}

function luaWindowDispatch(name: string, windowAddress: string, fields: string[]): string[] {
  return [
    'dispatch',
    `hl.dsp.window.${name}({ ${[...fields, `window = ${luaString(windowAddress)}`].join(', ')} })`,
  ];
}

function luaWindowSetProp(windowAddress: string, prop: string, value: string): string[] {
  return luaWindowDispatch('set_prop', windowAddress, [
    `prop = ${luaString(prop)}`,
    `value = ${luaString(value)}`,
  ]);
}

function luaString(value: string): string {
  return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
}

function roundPlacementBounds(
  bounds?: HyprlandPlacementBounds | null,
): HyprlandPlacementBounds | null {
  if (!bounds) {
    return null;
  }
  const rounded = {
    x: Math.round(bounds.x),
    y: Math.round(bounds.y),
    width: Math.round(bounds.width),
    height: Math.round(bounds.height),
  };
  return Number.isFinite(rounded.x) &&
    Number.isFinite(rounded.y) &&
    Number.isFinite(rounded.width) &&
    Number.isFinite(rounded.height) &&
    rounded.width > 0 &&
    rounded.height > 0
    ? rounded
    : null;
}

function isFiniteTuple(value: unknown): value is [number, number] {
  return (
    Array.isArray(value) &&
    value.length >= 2 &&
    typeof value[0] === 'number' &&
    typeof value[1] === 'number' &&
    Number.isFinite(value[0]) &&
    Number.isFinite(value[1])
  );
}

export function getHyprlandClientPlacementBounds(
  client: HyprlandPlacementClient,
): HyprlandPlacementBounds | null {
  if (!isFiniteTuple(client.at) || !isFiniteTuple(client.size)) {
    return null;
  }

  return roundPlacementBounds({
    x: client.at[0],
    y: client.at[1],
    width: client.size[0],
    height: client.size[1],
  });
}

export function hyprlandPlacementBoundsMatch(
  actual: HyprlandPlacementBounds | null,
  target: HyprlandPlacementBounds | null,
  tolerancePx = 1,
): boolean {
  const roundedActual = roundPlacementBounds(actual);
  const roundedTarget = roundPlacementBounds(target);
  if (!roundedActual || !roundedTarget) {
    return false;
  }

  return (
    Math.abs(roundedActual.x - roundedTarget.x) <= tolerancePx &&
    Math.abs(roundedActual.y - roundedTarget.y) <= tolerancePx &&
    Math.abs(roundedActual.width - roundedTarget.width) <= tolerancePx &&
    Math.abs(roundedActual.height - roundedTarget.height) <= tolerancePx
  );
}

function clientMatchesPlacementBounds(
  client: HyprlandPlacementClient,
  bounds: HyprlandPlacementBounds,
): boolean | null {
  const actual = getHyprlandClientPlacementBounds(client);
  return actual ? hyprlandPlacementBoundsMatch(actual, bounds) : null;
}

export function hasHyprlandWindowPlacementBoundsMismatch(options: {
  title: string;
  bounds?: HyprlandPlacementBounds | null;
  platform?: NodeJS.Platform;
  env?: NodeJS.ProcessEnv;
  pid?: number;
  execFileSync?: ExecFileSync;
}): boolean {
  if (!shouldAttemptHyprlandWindowPlacement(options.platform, options.env)) {
    return false;
  }

  const targetBounds = roundPlacementBounds(options.bounds);
  if (!targetBounds) {
    return false;
  }

  const run = options.execFileSync ?? execFileSync;
  try {
    const client = findHyprlandWindowForPlacement(readHyprlandPlacementClients(run), {
      pid: options.pid ?? process.pid,
      title: options.title,
    });
    if (!client) {
      return false;
    }
    return clientMatchesPlacementBounds(client, targetBounds) === false;
  } catch {
    return false;
  }
}

/**
 * Placement outcome for a single reconcile attempt.
 * - `applicable`: false when not running under Hyprland (nothing to retry).
 * - `clientFound`: whether a mapped Hyprland client matched. On Wayland the window maps
 *   asynchronously after `show()`, so early attempts can miss it — callers retry while this
 *   is false so the modal actually gets promoted above fullscreen mpv instead of staying
 *   invisible.
 * - `dispatched`: whether any hyprctl dispatch was issued.
 */
export interface HyprlandPlacementStatus {
  applicable: boolean;
  clientFound: boolean;
  dispatched: boolean;
}

export function ensureHyprlandWindowFloatingByTitle(options: {
  title: string;
  bounds?: HyprlandPlacementBounds | null;
  platform?: NodeJS.Platform;
  env?: NodeJS.ProcessEnv;
  pid?: number;
  promote?: boolean;
  execFileSync?: ExecFileSync;
}): boolean {
  return ensureHyprlandWindowFloatingByTitleWithStatus(options).dispatched;
}

export function ensureHyprlandWindowFloatingByTitleWithStatus(options: {
  title: string;
  bounds?: HyprlandPlacementBounds | null;
  platform?: NodeJS.Platform;
  env?: NodeJS.ProcessEnv;
  pid?: number;
  promote?: boolean;
  execFileSync?: ExecFileSync;
}): HyprlandPlacementStatus {
  if (!shouldAttemptHyprlandWindowPlacement(options.platform, options.env)) {
    return { applicable: false, clientFound: false, dispatched: false };
  }

  const run = options.execFileSync ?? execFileSync;
  try {
    const clients = readHyprlandPlacementClients(run);
    const client = findHyprlandWindowForPlacement(clients, {
      pid: options.pid ?? process.pid,
      title: options.title,
    });
    if (!client) {
      return { applicable: true, clientFound: false, dispatched: false };
    }

    const configProvider = detectHyprlandConfigProvider(run);
    const targetBounds = roundPlacementBounds(options.bounds);
    const shouldVerifyBounds =
      targetBounds !== null && clientMatchesPlacementBounds(client, targetBounds) === false;
    const dispatches = buildHyprlandPlacementDispatches(client, options.bounds, {
      configProvider,
      promote: options.promote,
    });
    for (const args of dispatches) {
      run('hyprctl', args, { stdio: 'ignore' });
    }
    if (shouldVerifyBounds) {
      try {
        const refreshedClient = findHyprlandWindowForPlacement(readHyprlandPlacementClients(run), {
          pid: options.pid ?? process.pid,
          title: options.title,
        });
        if (
          refreshedClient &&
          targetBounds &&
          clientMatchesPlacementBounds(refreshedClient, targetBounds) === false
        ) {
          for (const args of buildHyprlandPlacementDispatches(refreshedClient, targetBounds, {
            configProvider,
            promote: options.promote,
          })) {
            run('hyprctl', args, { stdio: 'ignore' });
          }
        }
      } catch {
        // Best-effort reconciliation: the initial placement dispatches already ran.
      }
    }
    return { applicable: true, clientFound: true, dispatched: dispatches.length > 0 };
  } catch {
    return { applicable: true, clientFound: false, dispatched: false };
  }
}

function detectHyprlandConfigProvider(run: ExecFileSync): HyprlandConfigProvider {
  try {
    return parseHyprlandConfigProvider(
      String(run('hyprctl', ['-j', 'status'], { encoding: 'utf-8' })),
    );
  } catch {
    return 'hyprlang';
  }
}

function parseHyprlandConfigProvider(output: string): HyprlandConfigProvider {
  const payloadStart = output.indexOf('{');
  if (payloadStart < 0) {
    return 'hyprlang';
  }

  const parsed = JSON.parse(output.slice(payloadStart)) as unknown;
  return isHyprlandStatusPayload(parsed) && parsed.configProvider === 'lua' ? 'lua' : 'hyprlang';
}

function isHyprlandStatusPayload(value: unknown): value is { configProvider?: unknown } {
  return Boolean(value) && typeof value === 'object';
}
