import { useEffect, useMemo, useReducer, useRef, useState } from "react";
import { API_BASE, request } from "../../app/api.js";
import { createReconnectingEventSource } from "../../app/event-stream.js";
import { projectConversation } from "../../conversation.js";
import { clearComposerDraft, loadComposerDraft, saveComposerDraft } from "./composer-draft.js";
import { buildComposerPayload, initialComposerOptions, loadComposerOptions, normalizeComposerModel, saveComposerOptions, updateComposerOption } from "./composer-state.js";
import { attachThreadWorkspace, decideSessionApproval, deleteQueuedSessionMessage, deleteSessionAttachment, listConversationCapabilities, sendSessionMessage, steerQueuedSessionMessage, stopSessionTurn, uploadSessionAttachment } from "./conversation-api.js";
import { initialConversationState, reduceConversationState, sessionOwnerId } from "./conversation-state.js";
import { createRequestLock } from "./conversation-submit.js";
import { createMessageAttemptTracker } from "./idempotent-action.js";
import { createConversationEventBatch } from "./conversation-event-batch.js";

const EVENT_NAMES = ["session.created", "session.status", "message.accepted", "session.output", "error", "approval.requested", "approval.resolved", "files.changed", "thread.token_usage", "turn.started", "turn.completed", "reasoning.delta", "plan.updated", "activity.started", "activity.updated", "activity.completed"];
const EMPTY_CAPABILITIES = { models: [], permissions: [], plugins: [], skills: [], errors: [] };
const CONVERSATION_SESSION_STATUSES = ["sending", "thinking", "running", "waiting_approval", "waiting_response", "interrupted", "failed"];

function capabilityLoadFailure() {
  return { ...EMPTY_CAPABILITIES, errors: [{ kind: "capabilities", message: "Capabilities unavailable" }] };
}

export function useConversation({ session, accountId, accounts = [], workspaceId, recoveryVersion = 0, onSessionUpdated, onFilesChanged, onFileActivity }) {
  const [state, dispatch] = useReducer(reduceConversationState, initialConversationState);
  const [composerOptions, setComposerOptions] = useState(() => loadComposerOptions(globalThis.localStorage, session?.id));
  const [attachments, setAttachments] = useState([]);
  const [selectedSkills, setSelectedSkills] = useState([]);
  const [capabilities, setCapabilities] = useState({ models: [], permissions: [], plugins: [], skills: [], errors: [] });
  const [now, setNow] = useState(() => Date.now());
  const [submitPending, setSubmitPending] = useState("");
  const [stopPending, setStopPending] = useState(false);
  const [queuedActionPending, setQueuedActionPending] = useState(null);
  const [approvalDecisionPending, setApprovalDecisionPending] = useState(null);
  const attachmentsRef = useRef([]);
  const cancelledAttachmentIdsRef = useRef(new Set());
  const submitLockRef = useRef(null);
  const queuedActionLockRef = useRef(null);
  const messageAttemptRef = useRef(null);
  const sessionRef = useRef(session);
  sessionRef.current = session;
  if (!submitLockRef.current) submitLockRef.current = createRequestLock();
  if (!queuedActionLockRef.current) queuedActionLockRef.current = createRequestLock();
  if (!messageAttemptRef.current) messageAttemptRef.current = createMessageAttemptTracker();
  const sourceRef = useRef(null);
  const conversation = useMemo(() => projectConversation(state.events), [state.events]);
  const loading = Boolean(session?.id && (state.sessionId !== session.id || state.loading));
  const displaySession = useMemo(() => {
    const projected = sessionForConversation(session, conversation);
    return projected ? { ...projected, selectedExecutionAccountId: accountId, executionAccounts: accounts } : projected;
  }, [session, conversation, accountId, accounts]);

  useEffect(() => {
    saveComposerOptions(globalThis.localStorage, composerOptions);
  }, [composerOptions.approvalPolicy, composerOptions.permissionProfile]);

  useEffect(() => {
    setComposerOptions(loadComposerOptions(globalThis.localStorage, session?.id));
  }, [session?.id]);

  useEffect(() => {
    if ((capabilities.models ?? []).length === 0) return;
    setComposerOptions((current) => {
      const normalized = normalizeComposerModel(current, capabilities.models);
      if (normalized !== current) saveComposerOptions(globalThis.localStorage, normalized, sessionRef.current?.id);
      return normalized;
    });
  }, [session?.id, capabilities.models]);

  useEffect(() => {
    attachmentsRef.current = attachments;
  }, [attachments]);

  useEffect(() => () => revokeAttachmentPreviews(attachmentsRef.current), []);

  useEffect(() => {
    if (session?.id && session.id !== state.sessionId) {
      submitLockRef.current = createRequestLock();
      queuedActionLockRef.current = createRequestLock();
      messageAttemptRef.current = createMessageAttemptTracker();
      setSubmitPending("");
      setStopPending(false);
      setQueuedActionPending(null);
      setApprovalDecisionPending(null);
      setSelectedSkills([]);
      setAttachments((current) => {
        revokeAttachmentPreviews(current);
        return [];
      });
      dispatch({
        type: "session.replaced",
        sessionId: session.id,
        events: [],
        message: loadComposerDraft(globalThis.localStorage, session.id)
      });
    }
  }, [session?.id, state.sessionId]);

  useEffect(() => {
    if (!state.sessionId) return;
    const targetSessionId = state.sessionId;
    const eventBatch = createConversationEventBatch({
      flush: (entries) => dispatch({ type: "events.received", sessionId: targetSessionId, entries })
    });
    sourceRef.current?.stop();
    const connection = createReconnectingEventSource({
      url: `${API_BASE}/sessions/${state.sessionId}/events`,
      onSource(source) {
        source.addEventListener("session.snapshot", (event) => {
          const data = JSON.parse(event.data);
          eventBatch.clear();
          sessionRef.current = data.session;
          onSessionUpdated(data.session);
          dispatch({ type: "snapshot.received", events: data.events });
        });
        for (const name of EVENT_NAMES) source.addEventListener(name, (event) => {
          if (typeof event.data !== "string") return;
          const data = JSON.parse(event.data);
          eventBatch.push({ id: crypto.randomUUID(), event: name, data, createdAt: new Date().toISOString() });
          if (name === "session.status" || name === "session.created") {
            sessionRef.current = data;
            onSessionUpdated(data);
          }
          if (name === "thread.token_usage") onSessionUpdated({ ...sessionRef.current, tokenUsage: data.tokenUsage });
          if (name === "files.changed") onFilesChanged(data.files ?? []);
          if (name === "activity.completed" && data.kind === "fileChange") void onFileActivity();
        });
      },
      onError: () => { void request("/auth/session").catch(() => {}); }
    });
    sourceRef.current = connection;
    return () => {
      eventBatch.clear();
      connection.stop();
    };
  }, [state.sessionId, recoveryVersion]);

  useEffect(() => {
    if (!session || !["starting", "running", "waiting_approval"].includes(session.status)) return;
    setNow(Date.now());
    const timer = setInterval(() => setNow(Date.now()), 1000);
    return () => clearInterval(timer);
  }, [session?.status]);

  useEffect(() => {
    if (!accountId) {
      setCapabilities(EMPTY_CAPABILITIES);
      return;
    }
    let cancelled = false;
    listConversationCapabilities(accountId, workspaceId)
      .then((result) => {
        if (!cancelled) setCapabilities(result);
      })
      .catch(() => {
        if (!cancelled) setCapabilities(capabilityLoadFailure());
      });
    return () => { cancelled = true; };
  }, [accountId, workspaceId, recoveryVersion]);

  function send(messageOverride) {
    const text = typeof messageOverride === "string" ? messageOverride : state.message;
    if (attachments.some((item) => item.pending) || (!text.trim() && attachments.length === 0)) return undefined;
    const requestSessionId = session.id;
    const effectiveSession = sessionForConversation(session, conversation);
    const pendingKind = effectiveSession.status === "running" ? "queue" : "send";
    return submitLockRef.current.run(async () => {
      const submittedAttachments = attachments;
      const submittedSkills = selectedSkills;
      const payload = buildComposerPayload({
        message: text,
        executionAccountId: accountId,
        options: composerOptions,
        attachments: submittedAttachments,
        selectedSkills: submittedSkills
      });
      const messageAttempt = messageAttemptRef.current;
      const clientMessageId = messageAttempt.begin(payload);
      const optimisticId = crypto.randomUUID();
      dispatch({ type: "message.submitted", id: optimisticId, text, queued: pendingKind === "queue", attachmentCount: submittedAttachments.length, attachments: submittedAttachments, createdAt: new Date().toISOString() });
      setAttachments([]);
      setSelectedSkills([]);
      setSubmitPending(pendingKind);
      try {
        const result = await sendSessionMessage(requestSessionId, { ...payload, clientMessageId });
        messageAttempt.succeeded(clientMessageId);
        clearComposerDraft(globalThis.localStorage, requestSessionId);
        if (!isCurrentSessionRequest(sessionRef, requestSessionId)) return result;
        sessionRef.current = result.session;
        onSessionUpdated(result.session);
        if (result.delivery?.disposition === "queued") dispatch({ type: "message.queued", id: optimisticId, keepOptimistic: true });
        return result;
      } catch (error) {
        messageAttempt.failed(clientMessageId, error);
        if (!isCurrentSessionRequest(sessionRef, requestSessionId)) throw error;
        dispatch({ type: "message.failed", id: optimisticId, text });
        setAttachments((current) => [...submittedAttachments, ...current]);
        setSelectedSkills((current) => [...submittedSkills, ...current]);
        throw error;
      } finally {
        if (isCurrentSessionRequest(sessionRef, requestSessionId)) setSubmitPending("");
      }
    });
  }
  function steerQueued(queuedMessageId) {
    return runQueuedAction(queuedMessageId, "steer", () => steerQueuedSessionMessage(session.id, queuedMessageId));
  }
  function deleteQueued(queuedMessageId) {
    return runQueuedAction(queuedMessageId, "delete", () => deleteQueuedSessionMessage(session.id, queuedMessageId));
  }
  function runQueuedAction(queuedMessageId, kind, operation) {
    return queuedActionLockRef.current.run(async () => {
      setQueuedActionPending({ id: queuedMessageId, kind });
      try {
        const result = await operation();
        onSessionUpdated(result.session);
        return result;
      } finally {
        setQueuedActionPending(null);
      }
    });
  }
  async function decide(approval, decision) {
    if (!session?.id || approval.status !== "pending" || approvalDecisionPending) return;
    const requestSessionId = session.id;
    setApprovalDecisionPending(approval.id);
    dispatch({
      type: "approval.decision.started",
      id: approval.id,
      eventId: crypto.randomUUID(),
      decision,
      createdAt: new Date().toISOString()
    });
    try {
      const result = await decideSessionApproval(requestSessionId, approval.id, decision);
      if (!isCurrentSessionRequest(sessionRef, requestSessionId)) return result;
      sessionRef.current = result.session;
      onSessionUpdated(result.session);
      return result;
    } catch (error) {
      if (isCurrentSessionRequest(sessionRef, requestSessionId)) {
        dispatch({
          type: "approval.decision.failed",
          id: approval.id,
          eventId: crypto.randomUUID(),
          createdAt: new Date().toISOString()
        });
      }
      throw error;
    } finally {
      if (isCurrentSessionRequest(sessionRef, requestSessionId)) setApprovalDecisionPending(null);
    }
  }
  async function stop() {
    if (!session?.id || stopPending) return;
    setStopPending(true);
    const optimisticStopId = crypto.randomUUID();
    dispatch({ type: "stop.requested", id: optimisticStopId, createdAt: new Date().toISOString() });
    try {
      const result = await stopSessionTurn(session.id);
      onSessionUpdated(result.session);
      return result;
    } catch (error) {
      dispatch({ type: "stop.failed", id: optimisticStopId });
      throw error;
    } finally {
      setStopPending(false);
    }
  }
  async function attach(selectedWorkspaceId = workspaceId) {
    if (!session?.codexThreadId || !selectedWorkspaceId) return;
    const result = await attachThreadWorkspace(sessionOwnerId(session, accountId), session.codexThreadId, selectedWorkspaceId);
    onSessionUpdated(result.session);
    replace(result.session.id, result.events);
  }
  async function uploadAttachment(file) {
    if (!session?.id || !file) return;
    const requestSessionId = session.id;
    const localId = crypto.randomUUID();
    const previewUrl = URL.createObjectURL(file);
    const localAttachment = {
      id: localId,
      kind: "image",
      name: file.name,
      mimeType: file.type,
      size: file.size,
      previewUrl,
      pending: true
    };
    setAttachments((current) => [...current, localAttachment]);
    try {
      const dataBase64 = await fileToBase64(file);
      if (!isCurrentSessionRequest(sessionRef, requestSessionId)) return;
      const result = await uploadSessionAttachment(requestSessionId, {
        name: file.name,
        mimeType: file.type,
        dataBase64
      });
      if (!isCurrentSessionRequest(sessionRef, requestSessionId)) return result;
      if (cancelledAttachmentIdsRef.current.delete(localId)) {
        await deleteSessionAttachment(requestSessionId, result.attachment.id).catch(() => {});
        return result;
      }
      setAttachments((current) => current.map((item) => item.id === localId
        ? { ...result.attachment, previewUrl }
        : item));
      return result;
    } catch (error) {
      cancelledAttachmentIdsRef.current.delete(localId);
      if (!isCurrentSessionRequest(sessionRef, requestSessionId)) throw error;
      setAttachments((current) => current.filter((item) => item.id !== localId));
      URL.revokeObjectURL(previewUrl);
      throw error;
    }
  }
  async function removeAttachment(id) {
    const requestSessionId = sessionRef.current?.id;
    const removed = attachmentsRef.current.find((item) => item.id === id);
    if (!removed || !requestSessionId) return;
    if (removed.pending) cancelledAttachmentIdsRef.current.add(id);
    else await deleteSessionAttachment(requestSessionId, removed.id);
    setAttachments((current) => {
      if (removed?.previewUrl) URL.revokeObjectURL(removed.previewUrl);
      return current.filter((item) => item.id !== id);
    });
  }
  function setComposerOption(key, value) { setComposerOptions((current) => updateComposerOption(current, key, value, globalThis.localStorage, sessionRef.current?.id)); }
  function toggleSkill(skill) {
    setSelectedSkills((current) => current.some((item) => item.name === skill.name)
      ? current.filter((item) => item.name !== skill.name)
      : [...current, skill]);
  }
  function replace(sessionId, events, options) {
    const restoredOptions = loadComposerOptions(globalThis.localStorage, sessionId);
    const nextOptions = options
      ? { ...initialComposerOptions, ...restoredOptions, ...options }
      : restoredOptions;
    if (options) saveComposerOptions(globalThis.localStorage, nextOptions, sessionId);
    setComposerOptions(nextOptions);
    dispatch({
      type: "session.replaced",
      sessionId,
      events,
      loaded: true,
      message: loadComposerDraft(globalThis.localStorage, sessionId)
    });
  }
  function setMessage(message, targetSessionId = state.sessionId) {
    saveComposerDraft(globalThis.localStorage, targetSessionId, message);
    if (targetSessionId === state.sessionId) dispatch({ type: "message.changed", message });
  }
  return {
    ...state,
    loading,
    conversation,
    displaySession,
    now,
    capabilities,
    composerOptions,
    attachments,
    selectedSkills,
    submitPending,
    stopPending,
    queuedActionPending,
    approvalDecisionPending,
    replace,
    send,
    stop,
    steerQueued,
    deleteQueued,
    decide,
    attach,
    uploadAttachment,
    removeAttachment,
    setComposerOption,
    toggleSkill,
    setMessage
  };
}

function sessionForConversation(session, conversation) {
  const latestTurn = conversation.at(-1);
  if (!session) return session;
  if (CONVERSATION_SESSION_STATUSES.includes(latestTurn?.status)) return { ...session, status: latestTurn.status };
  return session;
}

function fileToBase64(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(String(reader.result).split(",")[1] ?? "");
    reader.onerror = () => reject(reader.error);
    reader.readAsDataURL(file);
  });
}

function revokeAttachmentPreviews(items) {
  for (const item of items ?? []) {
    if (item?.previewUrl) URL.revokeObjectURL(item.previewUrl);
  }
}

function isCurrentSessionRequest(sessionRef, requestSessionId) {
  return Boolean(requestSessionId && sessionRef.current?.id === requestSessionId);
}
