// *****************************************************************************
// Copyright (C) 2026 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import { inject, injectable } from '@theia/core/shared/inversify';
import { ChatResponsePartRenderer } from '@theia/ai-chat-ui/lib/browser/chat-response-part-renderer';
import { ResponseNode } from '@theia/ai-chat-ui/lib/browser/chat-tree-view';
import { ChatResponseContent, ToolCallChatResponseContent } from '@theia/ai-chat/lib/common';
import { ReactNode } from '@theia/core/shared/react';
import * as React from '@theia/core/shared/react';
import { UntitledResourceResolver } from '@theia/core';
import { codicon, ContextMenuRenderer, KeybindingRegistry, OpenerService } from '@theia/core/lib/browser';
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { ThemeService } from '@theia/core/lib/browser/theming';
import { nls } from '@theia/core/lib/common/nls';
import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
import { MarkdownWithMermaid } from '@theia/ai-chat-ui/lib/browser/chat-response-renderer/mermaid-rendering';
import { ToolConfirmationKeybindingHints, withToolCallConfirmation } from '@theia/ai-chat-ui/lib/browser/chat-response-renderer/tool-confirmation';
import {
    APPROVE_LATEST_TOOL_CONFIRMATION_COMMAND,
    DENY_LATEST_TOOL_CONFIRMATION_COMMAND
} from '@theia/ai-chat-ui/lib/browser/tool-confirmation-keybinding-contribution';
import { ToolConfirmationManager } from '@theia/ai-chat/lib/browser/chat-tool-preference-bindings';
import { PendingToolConfirmationTracker } from '@theia/ai-chat/lib/browser/pending-tool-confirmation-tracker';
import { ToolInvocationRegistry } from '@theia/ai-core';
import { UserInteractionTool } from './user-interaction-tool';
import {
    USER_INTERACTION_FUNCTION_ID,
    UserInteractionArgs,
    UserInteractionLink,
    UserInteractionResult,
    UserInteractionStep,
    UserInteractionStepResult,
    buildDiffLabel,
    isEmptyContentRef,
    parseUserInteractionArgs,
    parseUserInteractionInput,
    parseUserInteractionResult,
    resolveContentRef,
} from '../common/user-interaction-tool';

interface StepState {
    value?: string;
    comments: string[];
}

interface UserInteractionComponentProps {
    args: UserInteractionArgs;
    toolCallId: string;
    tool: UserInteractionTool;
    finished: boolean;
    canceled: boolean;
    result: UserInteractionResult | undefined;
    /**
     * Called whenever the user changes any step state. The parent persists this partial
     * result on the response (so it survives chat-session reloads) and pushes it to the
     * tool (so a synchronous cancellation can return it instead of all-skipped).
     */
    onPartialResult: (result: UserInteractionResult) => void;
    openerService: OpenerService;
    themeService: ThemeService;
    clipboardService: ClipboardService;
    editorProvider: MonacoEditorProvider;
    untitledResourceResolver: UntitledResourceResolver;
}

const UserInteractionComponent: React.FC<UserInteractionComponentProps> = ({
    args, toolCallId, tool, finished, canceled, result, onPartialResult, openerService, themeService, clipboardService, editorProvider, untitledResourceResolver
}) => {
    const steps = args.interactions;
    const stepCount = steps.length;
    const [currentStep, setCurrentStep] = React.useState(0);
    // The tool's result (partial or final) is the single source of truth for step states.
    const [stepStates, setStepStates] = React.useState<StepState[]>(() => {
        if (result) {
            return steps.map((_, i) => ({
                value: result.steps[i]?.value,
                comments: result.steps[i]?.comments ? [...result.steps[i].comments!] : []
            }));
        }
        return steps.map(() => ({ comments: [] }));
    });
    // Mirror stepStates into a ref so synchronous readers (cancellation fallback,
    // terminal handlers) always see the latest value. The ref is updated synchronously
    // by every code path that writes to stepStates, which also keeps these handlers
    // free of `stepStates` deps and avoids state-updater side effects.
    const stepStatesRef = React.useRef(stepStates);
    const [pendingComment, setPendingComment] = React.useState('');

    const activeStep: UserInteractionStep | undefined = steps[currentStep];
    const isLastStep = currentStep === stepCount - 1;

    // A finished tool call has no live handler anymore (completion, cancellation, or
    // restoration of a previously-pending interaction). Lock all inputs in that case.
    const isFinal = finished || canceled;

    // Auto-open the active step's links the first time the user reaches it.
    // Going Back and then Forward must not re-open them.
    const visitedStepsRef = React.useRef<Set<number>>(new Set());
    React.useEffect(() => {
        if (isFinal || !activeStep || visitedStepsRef.current.has(currentStep)) {
            return;
        }
        visitedStepsRef.current.add(currentStep);
        const links = activeStep.links ?? [];
        for (const link of links) {
            if (link.autoOpen) {
                tool.openLink(link).catch(err => console.warn('Failed to auto-open user-interaction link:', err));
            }
        }
    }, [currentStep, activeStep, isFinal, tool]);

    const buildResult = React.useCallback((completed: boolean, states: StepState[]): UserInteractionResult => ({
        completed,
        steps: steps.map((step, i) => {
            const state = states[i];
            const stepResult: UserInteractionStepResult = { title: step.title };
            if (state?.value !== undefined) {
                stepResult.value = state.value;
            }
            if (state?.comments && state.comments.length > 0) {
                stepResult.comments = [...state.comments];
            }
            // For partial/cancel results, mark untouched steps as skipped so the LLM
            // can distinguish "answered" from "not answered" if the interaction never
            // completes.
            if (!completed && stepResult.value === undefined && stepResult.comments === undefined) {
                stepResult.skipped = true;
            }
            return stepResult;
        })
    }), [steps]);

    const updateStepState = React.useCallback((stepIndex: number, updater: (prev: StepState) => StepState) => {
        const next = stepStatesRef.current.slice();
        next[stepIndex] = updater(next[stepIndex]);
        stepStatesRef.current = next;
        setStepStates(next);
        onPartialResult(buildResult(false, next));
    }, [buildResult, onPartialResult]);

    const isSingleStep = stepCount === 1;
    const hasOptions = !!activeStep?.options && activeStep.options.length > 0;

    const handleOptionClick = React.useCallback((value: string) => {
        if (isFinal) {
            return;
        }
        if (isSingleStep) {
            const next: StepState[] = [{ ...stepStatesRef.current[0], value }];
            stepStatesRef.current = next;
            setStepStates(next);
            tool.completeInteraction(toolCallId, buildResult(true, next));
            return;
        }
        updateStepState(currentStep, prev => ({
            ...prev,
            value: prev.value === value ? undefined : value
        }));
    }, [buildResult, currentStep, isFinal, isSingleStep, tool, toolCallId, updateStepState]);

    const handleAddComment = React.useCallback(() => {
        const trimmed = pendingComment.trim();
        if (isFinal || !trimmed) {
            return;
        }
        updateStepState(currentStep, prev => ({
            ...prev,
            comments: [...prev.comments, trimmed]
        }));
        setPendingComment('');
    }, [currentStep, isFinal, pendingComment, updateStepState]);

    const handleRemoveComment = React.useCallback((commentIndex: number) => {
        if (isFinal) {
            return;
        }
        updateStepState(currentStep, prev => ({
            ...prev,
            comments: prev.comments.filter((_, i) => i !== commentIndex)
        }));
    }, [currentStep, isFinal, updateStepState]);

    const handleCommentKeyDown = React.useCallback((e: React.KeyboardEvent) => {
        if (e.key === 'Enter') {
            e.preventDefault();
            handleAddComment();
        }
    }, [handleAddComment]);

    const goToStep = React.useCallback((idx: number) => {
        if (idx < 0 || idx >= stepCount) {
            return;
        }
        setCurrentStep(idx);
        setPendingComment('');
    }, [stepCount]);

    const handleAdvance = React.useCallback(() => {
        if (isLastStep) {
            if (!isFinal) {
                tool.completeInteraction(toolCallId, buildResult(true, stepStatesRef.current));
            }
            return;
        }
        setCurrentStep(idx => idx + 1);
        setPendingComment('');
    }, [buildResult, isFinal, isLastStep, tool, toolCallId]);

    const handleBack = React.useCallback(() => {
        if (currentStep === 0) {
            return;
        }
        setCurrentStep(idx => idx - 1);
        setPendingComment('');
    }, [currentStep]);

    if (!activeStep) {
        return undefined;
    }

    const activeState = stepStates[currentStep];
    const stepLabel = nls.localizeByDefault('Step {0} of {1}', currentStep + 1, stepCount);
    const advanceLabel = isLastStep
        ? nls.localize('theia/ai-ide/userInteractionFinishStep', 'Finish')
        : nls.localizeByDefault('Next');

    const showAdvanceRow = !isSingleStep;

    return (
        <div className='tool-call container user-interaction-wizard'>
            <div className='tool-call header'>
                <span className={codicon('comment-discussion')} />
                <span className='user-interaction-tool title'>{activeStep.title}</span>
                {(() => {
                    // A completed result is authoritative and persists even if the
                    // chat is later canceled. While the tool is live we may already
                    // have a partial result (`completed: false`) so distinguish
                    // "still waiting" from "canceled" using `finished`/`canceled`:
                    // both are only true once no live handler is around.
                    const status: 'completed' | 'canceled' | 'waiting' =
                        result?.completed === true ? 'completed' :
                            finished || canceled ? 'canceled' :
                                'waiting';
                    if (status === 'completed') {
                        return (
                            <span className='user-interaction-tool status completed'>
                                <i className={codicon('check')} />
                                {nls.localizeByDefault('Completed')}
                            </span>
                        );
                    }
                    if (status === 'canceled') {
                        return (
                            <span className='user-interaction-tool status canceled'>
                                <i className={codicon('close')} />
                                {nls.localizeByDefault('Canceled')}
                            </span>
                        );
                    }
                    return (
                        <span className='user-interaction-tool status waiting' role='status' aria-live='polite'>
                            {nls.localize('theia/ai/chat-ui/chat-view-tree-widget/waitingForInput', 'Waiting for input')}
                        </span>
                    );
                })()}
            </div>
            {!isSingleStep && <StepProgress current={currentStep} total={stepCount} onSelect={goToStep} steps={steps} />}
            {activeStep.links && activeStep.links.length > 0 && (
                <div className='user-interaction-tool links'>
                    {activeStep.links.map((link, i) => (
                        <LinkButton
                            key={i}
                            link={link}
                            onClick={() => tool.openLink(link).catch(err => console.warn('Failed to open user-interaction link:', err))}
                        />
                    ))}
                </div>
            )}
            {/* Render every step's message up front, each in its own keyed container and hidden when inactive.
                Mounting them all means any Mermaid diagrams render once (while hidden, so they add no layout
                height) instead of rendering on first navigation to a step. A diagram that renders asynchronously
                while its step is visible changes the row height a tick later, which makes the virtualized chat
                scroll and can unmount the interaction; pre-rendering avoids that, and keeping each step mounted
                also preserves its view state (zoom, source mode, ...) across navigation without bleeding into
                other steps. */}
            {steps.map((step, i) =>
                <div key={i} className='user-interaction-tool message' hidden={i !== currentStep}>
                    <MarkdownWithMermaid
                        content={step.message ?? ''}
                        openerService={openerService}
                        themeService={themeService}
                        clipboardService={clipboardService}
                        editorProvider={editorProvider}
                        untitledResourceResolver={untitledResourceResolver} />
                </div>
            )}
            {hasOptions && (
                <div className='user-interaction-tool options'>
                    {activeStep.options!.map((option, i) => {
                        const isSelected = activeState.value === option.value;
                        const className = 'user-interaction-tool option-button theia-button '
                            + (isSelected ? 'main selected' : 'secondary');
                        const label = option.buttonLabel || option.text;
                        return (
                            <button
                                key={i}
                                className={className}
                                onClick={() => handleOptionClick(option.value)}
                                disabled={isFinal}
                                title={option.description}
                                aria-pressed={isSelected}
                            >
                                {/* Hidden bold copy reserves the width needed for the bold "selected" state
                                    so the button does not grow when the label switches to bold. */}
                                <span className='user-interaction-tool option-button-bold-spacer' aria-hidden='true'>{label}</span>
                                <span className='user-interaction-tool option-button-label'>{label}</span>
                            </button>
                        );
                    })}
                </div>
            )}
            {!isSingleStep && (
                <div className='user-interaction-tool comment-section'>
                    {!isFinal && (
                        <div className='user-interaction-tool comment-input-row'>
                            <input
                                type='text'
                                className='theia-input user-interaction-tool comment-input-field'
                                placeholder={nls.localize('theia/ai-ide/userInteractionCommentPlaceholder', 'Add a comment for this step...')}
                                value={pendingComment}
                                onChange={e => setPendingComment(e.target.value)}
                                onKeyDown={handleCommentKeyDown}
                            />
                            <button
                                className='theia-button secondary user-interaction-tool comment-submit'
                                onClick={handleAddComment}
                                disabled={!pendingComment.trim()}
                            >
                                {nls.localizeByDefault('Comment')}
                            </button>
                        </div>
                    )}
                    {activeState.comments.length > 0 && (
                        <ul className='user-interaction-tool comment-list'>
                            {activeState.comments.map((comment, i) => (
                                <li key={i} className='user-interaction-tool comment-item'>
                                    <span className='user-interaction-tool comment-text'>{comment}</span>
                                    {!isFinal && (
                                        <button
                                            className='user-interaction-tool comment-remove'
                                            onClick={() => handleRemoveComment(i)}
                                            title={nls.localizeByDefault('Remove')}
                                            aria-label={nls.localizeByDefault('Remove')}
                                        >
                                            <i className={codicon('close')} />
                                        </button>
                                    )}
                                </li>
                            ))}
                        </ul>
                    )}
                </div>
            )}
            {showAdvanceRow && (
                <div className='user-interaction-tool advance-row'>
                    {!isSingleStep && (
                        <>
                            <button
                                className='theia-button secondary user-interaction-tool back-button'
                                onClick={handleBack}
                                disabled={currentStep === 0}
                                title={nls.localizeByDefault('Back')}
                            >
                                <i className={codicon('arrow-left')} />
                                {nls.localizeByDefault('Back')}
                            </button>
                            <span className='user-interaction-tool page-counter' aria-label={stepLabel}>
                                {currentStep + 1} / {stepCount}
                            </span>
                        </>
                    )}
                    <button
                        className='theia-button main user-interaction-tool advance-button'
                        onClick={handleAdvance}
                        disabled={isFinal && isLastStep}
                    >
                        {advanceLabel}
                        <i className={codicon(isLastStep ? 'check' : 'arrow-right')} />
                    </button>
                </div>
            )}
        </div>
    );
};

const StepProgress: React.FC<{
    current: number;
    total: number;
    onSelect: (index: number) => void;
    steps: UserInteractionStep[];
}> = ({ current, total, onSelect, steps }) => (
    <div className='user-interaction-tool progress'>
        {Array.from({ length: total }).map((_, i) => {
            const label = nls.localize(
                'theia/ai-ide/userInteractionGoToStep',
                'Go to step {0}: {1}',
                i + 1,
                steps[i]?.title ?? ''
            );
            return (
                <button
                    key={i}
                    type='button'
                    className={'user-interaction-tool progress-dot'
                        + (i < current ? ' done' : '')
                        + (i === current ? ' active' : '')}
                    onClick={() => onSelect(i)}
                    title={label}
                    aria-label={label}
                    aria-current={i === current ? 'step' : undefined}
                />
            );
        })}
    </div>
);

const LinkButton: React.FC<{ link: UserInteractionLink; onClick: () => void }> = ({ link, onClick }) => {
    const isDiff = link.rightRef !== undefined;
    const icon = isDiff ? codicon('diff') : codicon('go-to-file');
    const left = resolveContentRef(link.ref);
    let label: string;
    if (link.label) {
        label = link.label;
    } else if (isDiff) {
        label = buildDiffLabel(left, resolveContentRef(link.rightRef!));
    } else {
        label = isEmptyContentRef(left)
            ? (left.label || nls.localize('theia/ai-ide/userInteractionEmpty', 'Empty'))
            : left.path;
    }

    return (
        <button className='user-interaction-tool link-button' onClick={onClick}>
            <i className={icon} />
            <span>{label}</span>
        </button>
    );
};

const UserInteractionWithConfirmation = withToolCallConfirmation(UserInteractionComponent);

const StreamingProgress: React.FC<{ title: string; stepCount: number }> = ({ title, stepCount }) => {
    let label: string;
    if (title && stepCount > 0) {
        label = nls.localize('theia/ai-ide/userInteractionPreparingSteps', 'Preparing: {0} ({1} steps)', title, stepCount);
    } else if (title) {
        label = nls.localize('theia/ai-ide/userInteractionPreparingTitle', 'Preparing: {0}', title);
    } else {
        label = nls.localize('theia/ai-ide/userInteractionPreparing', 'Preparing user interaction...');
    }
    return (
        <div className='tool-call container'>
            <div className='tool-call header pending'>
                <span className={codicon('comment-discussion')} />
                <span className={`${codicon('loading')} theia-animation-spin`} />
                <span className='user-interaction-tool pending-text'>{label}</span>
            </div>
        </div>
    );
};

const MalformedInteraction: React.FC<{ message: string }> = ({ message }) => (
    <div className='tool-call container'>
        <div className='tool-call header error'>
            <span className={codicon('comment-discussion')} />
            <span className='user-interaction-tool title'>
                {nls.localize('theia/ai-ide/userInteractionMalformed', 'User interaction could not be displayed')}
            </span>
        </div>
        <div className='tool-call error-message'>{message}</div>
    </div>
);

interface ToolErrorResult { error: string }
function parseToolErrorResult(raw: unknown): ToolErrorResult | undefined {
    let candidate: unknown = raw;
    if (typeof raw === 'string') {
        try {
            candidate = JSON.parse(raw);
        } catch {
            return undefined;
        }
    }
    if (candidate && typeof candidate === 'object' && typeof (candidate as { error?: unknown }).error === 'string') {
        return candidate as ToolErrorResult;
    }
    return undefined;
}

@injectable()
export class UserInteractionToolRenderer implements ChatResponsePartRenderer<ToolCallChatResponseContent> {

    @inject(ToolConfirmationManager)
    protected toolConfirmationManager: ToolConfirmationManager;

    @inject(ContextMenuRenderer)
    protected contextMenuRenderer: ContextMenuRenderer;

    @inject(ToolInvocationRegistry)
    protected toolInvocationRegistry: ToolInvocationRegistry;

    @inject(UserInteractionTool)
    protected userInteractionTool: UserInteractionTool;

    @inject(OpenerService)
    protected openerService: OpenerService;

    @inject(ThemeService)
    protected themeService: ThemeService;

    @inject(ClipboardService)
    protected clipboardService: ClipboardService;

    @inject(MonacoEditorProvider)
    protected editorProvider: MonacoEditorProvider;

    @inject(UntitledResourceResolver)
    protected untitledResourceResolver: UntitledResourceResolver;

    @inject(PendingToolConfirmationTracker)
    protected pendingToolConfirmationTracker: PendingToolConfirmationTracker;

    @inject(KeybindingRegistry)
    protected keybindingRegistry: KeybindingRegistry;

    canHandle(response: ChatResponseContent): number {
        if (ToolCallChatResponseContent.is(response) && response.name === USER_INTERACTION_FUNCTION_ID) {
            return 20;
        }
        return -1;
    }

    render(response: ToolCallChatResponseContent, parentNode: ResponseNode): ReactNode {
        const args = parseUserInteractionArgs(response.arguments);

        if (!args || !response.id) {
            // The tool already returned a result but the args don't validate: this
            // is a malformed invocation (e.g., the agent sent a step the tool
            // rejected, or arguments that fail shared parsing). Show an error state
            // instead of a perpetual loading spinner.
            if (response.result !== undefined) {
                const error = parseToolErrorResult(response.result);
                const message = error?.error
                    ?? nls.localize('theia/ai-ide/userInteractionMalformedFallback', 'The arguments could not be parsed.');
                return <MalformedInteraction message={message} />;
            }
            const input = parseUserInteractionInput(response.arguments);
            return <StreamingProgress title={input.title} stepCount={input.stepCount} />;
        }

        const chatId = parentNode.sessionId;
        const toolRequest = this.toolInvocationRegistry.getFunction(USER_INTERACTION_FUNCTION_ID);
        const confirmationMode = this.toolConfirmationManager.getConfirmationMode(
            USER_INTERACTION_FUNCTION_ID, chatId, toolRequest
        );

        return (
            <UserInteractionWithConfirmation
                args={args}
                toolCallId={response.id}
                tool={this.userInteractionTool}
                finished={response.finished}
                canceled={parentNode.response.isCanceled}
                result={parseUserInteractionResult(response.result)}
                onPartialResult={partial => {
                    this.userInteractionTool.recordPartial(response.id!, partial);
                    response.updateResult(JSON.stringify(partial));
                }}
                openerService={this.openerService}
                themeService={this.themeService}
                clipboardService={this.clipboardService}
                editorProvider={this.editorProvider}
                untitledResourceResolver={this.untitledResourceResolver}
                toolConfirmation={{
                    response,
                    confirmationMode,
                    toolConfirmationManager: this.toolConfirmationManager,
                    toolRequest,
                    chatId,
                    requestCanceled: parentNode.response.isCanceled,
                    contextMenuRenderer: this.contextMenuRenderer,
                    openerService: this.openerService,
                    pendingTracker: this.pendingToolConfirmationTracker,
                    keybindingHints: this.getKeybindingHints()
                }}
            />
        );
    }

    protected getKeybindingHints(): ToolConfirmationKeybindingHints {
        const allow = this.formatKeybinding(APPROVE_LATEST_TOOL_CONFIRMATION_COMMAND.id);
        const deny = this.formatKeybinding(DENY_LATEST_TOOL_CONFIRMATION_COMMAND.id);
        return { allow, deny };
    }

    protected formatKeybinding(commandId: string): string | undefined {
        const bindings = this.keybindingRegistry.getKeybindingsForCommand(commandId);
        if (!bindings.length) {
            return undefined;
        }
        return this.keybindingRegistry.acceleratorFor(bindings[0], '+').join('+');
    }
}
