import type { ISsrNode, SSRContainer } from '../../ssr/ssr-types';
import { runTask, Task, TaskFlags, type TaskFn } from '../../use/use-task';
import { SsrNodeFlags, type Container } from '../types';
import { ELEMENT_PROPS, ELEMENT_SEQ } from '../utils/markers';
import type { ValueOrPromise } from '../utils/types';
import { ChoreBits } from '../vnode/enums/chore-bits.enum';
import { logWarn } from '../utils/log';
import { serializeAttribute } from '../utils/styles';
import { NODE_PROPS_DATA_KEY } from './cursor-props';
import type { NodeProp } from '../../reactive-primitives/subscription-data';
import { isSignal, type Signal } from '../../reactive-primitives/signal.public';
import { executeCompute } from './chore-execution';
import { isPromise } from '../utils/promises';
import { type Props } from '../jsx/jsx-runtime';
import type { EachProps } from '../../control-flow/each';
import { _getProps } from '../jsx/props-proxy';
import type { QRLInternal } from '../qrl/qrl-class';
import type { JSXNode } from '../jsx/types/jsx-node';
import { _jsxSorted } from '../../internal';
import { untrack } from '../../use/use-core';
import { isDev } from '@qwik.dev/core/build';

/** @internal */
export function _executeSsrChores(
  container: SSRContainer,
  ssrNode: ISsrNode
): ValueOrPromise<void> {
  if (!(ssrNode.flags & SsrNodeFlags.Updatable)) {
    if (ssrNode.dirty & ChoreBits.NODE_PROPS) {
      executeNodePropChore(container, ssrNode);
    }
    if (ssrNode.dirty & ChoreBits.COMPUTE) {
      executeCompute(ssrNode, container);
    }
    if (
      isDev &&
      ssrNode.dirty & ChoreBits.DIRTY_MASK &&
      !(ssrNode.flags & SsrNodeFlags.WarnedStreamedChore)
    ) {
      // Warn once per host; a re-dirtied streamed host would otherwise spam every tick.
      ssrNode.flags |= SsrNodeFlags.WarnedStreamedChore;
      // We are running on the server.
      // On server we can't schedule task for a different host!
      // Server is SSR, and therefore scheduling for anything but the current host
      // implies that things need to be re-run and that is not supported because of streaming.
      const warningMessage = `A chore was scheduled on a host element that has already been streamed to the client.
        This can lead to inconsistencies between Server-Side Rendering (SSR) and Client-Side Rendering (CSR).
        
        Problematic chore:
          - Host: ${ssrNode.toString()}
          - Nearest element location: ${ssrNode.currentFile}
        
        This is often caused by modifying a signal in an already rendered component during SSR.`;
      logWarn(warningMessage);
    }
    ssrNode.dirty &= ~ChoreBits.DIRTY_MASK;
    return;
  }

  let promise: ValueOrPromise<void> | null = null;
  if (ssrNode.dirty & ChoreBits.TASKS) {
    const result = executeTasksChore(container, ssrNode);
    if (isPromise(result)) {
      promise = result;
    }
  }

  if (ssrNode.dirty & ChoreBits.RECONCILE) {
    const result = executeReconcileChore(container, ssrNode);
    promise = promise ? promise.then(() => result) : result;
  }

  // In SSR, we don't handle the COMPONENT bit here.
  // During initial render, if a task completes and marks the component dirty,
  // we want to leave the COMPONENT bit set so that executeComponent can detect
  // it after $waitOn$ completes and re-execute the component function.
  // executeComponent will clear the bit after re-executing.

  // Clear all dirty bits EXCEPT COMPONENT
  ssrNode.dirty &= ~(ChoreBits.DIRTY_MASK & ~ChoreBits.COMPONENT);

  if (promise) {
    return promise;
  }
}

function executeTasksChore(container: Container, ssrNode: ISsrNode): ValueOrPromise<void> | null {
  ssrNode.dirty &= ~ChoreBits.TASKS;
  const elementSeq = ssrNode.getProp(ELEMENT_SEQ);
  if (!elementSeq || elementSeq.length === 0) {
    // No tasks to execute, clear the bit
    return null;
  }
  let promise: ValueOrPromise<void> | null = null;
  for (let i = 0; i < elementSeq.length; i++) {
    const item = elementSeq[i];
    if (item instanceof Task) {
      const task = item as Task<TaskFn, TaskFn>;

      // Skip if task is not dirty
      if (!(task.$flags$ & TaskFlags.DIRTY)) {
        continue;
      }

      const result = runTask(task, container, ssrNode);
      promise = promise ? promise.then(() => result as Promise<void>) : (result as Promise<void>);
    }
  }
  return promise;
}

export function executeNodePropChore(container: SSRContainer, ssrNode: ISsrNode): void {
  ssrNode.dirty &= ~ChoreBits.NODE_PROPS;

  const allPropData = ssrNode.getProp(NODE_PROPS_DATA_KEY) as Map<string, NodeProp> | null;
  if (!allPropData || allPropData.size === 0) {
    return;
  }

  for (const [property, nodeProp] of allPropData.entries()) {
    let value: Signal<any> | string = nodeProp.value;
    if (isSignal(value)) {
      // TODO: Handle async signals (promises) - need to track pending async prop data
      value = value.value as any;
    }
    const serializedValue = serializeAttribute(property, value, nodeProp.scopedStyleIdPrefix);
    container.addBackpatchEntry(ssrNode.id, property, serializedValue);
  }
}

export async function executeReconcileChore(
  container: SSRContainer,
  ssrNode: ISsrNode
): Promise<void> {
  ssrNode.dirty &= ~ChoreBits.RECONCILE;
  const host = ssrNode;
  const props = container.getHostProp<Props | null>(host, ELEMENT_PROPS) || null;
  if (!props) {
    return;
  }
  let items = _getProps(props, 'items' satisfies keyof EachProps<any>) as any[];
  if (isSignal(items)) {
    items = untrack(items) as any[];
  }
  const keyOf = (await (
    _getProps(props, 'key$' satisfies keyof EachProps<any>) as QRLInternal<
      (item: any, index: number) => string
    >
  ).resolve()) as (item: any, index: number) => string;
  const itemFn = (await (
    _getProps(props, 'item$' satisfies keyof EachProps<any>) as QRLInternal<
      (item: any, index: number) => JSXNode
    >
  ).resolve()) as (item: any, index: number) => JSXNode;
  const children: JSXNode[] = [];
  for (let i = 0; i < items.length; i++) {
    const item = items[i];
    const jsx = itemFn(item, i)!;
    const key = keyOf(item, i);
    jsx.key = key;
    children.push(jsx);
  }
  await container.renderJSX(children, {
    currentStyleScoped: null,
    parentComponentFrame: container.getComponentFrame(0),
  });
}
