/**
 * Form Input Tool - Set values in form elements
 */

import { MCPServer } from '../mcp-server';
import { MCPToolDefinition, MCPResult, ToolHandler, ToolContext, hasBudget } from '../types/mcp';
import { TOOL_ANNOTATIONS } from '../types/tool-annotations';
import { getSessionManager } from '../session-manager';
import { getRefIdManager, formatStaleRefError, makeStaleRefError } from '../utils/ref-id-manager';
import { withDomDelta } from '../utils/dom-delta';
import { isPilotEnabled } from '../harness/flags';
import { captureElementReplayStep, shouldCaptureReplayArtifact } from './_shared/replay-recorder';
import { appendReturnAfterState, parseReturnAfterState, RETURN_AFTER_STATE_SCHEMA } from './_shared/return-after-state';

async function resolveMaybeVault(value: unknown): Promise<{ value: unknown; token?: string; plaintext?: string; error?: MCPResult }> {
  if (typeof value !== 'string' || !value.startsWith('vault://') || !isPilotEnabled()) return { value };
  try {
    // eslint-disable-next-line @typescript-eslint/no-var-requires
    const vault = require('../pilot/credentials/store') as typeof import('../pilot/credentials/store');
    const resolved = await vault.resolveVaultValue(value);
    return { value: resolved.value, token: resolved.token, plaintext: String(resolved.value) };
  } catch (error) {
    const code = error && typeof error === 'object' && 'code' in error ? String((error as { code: unknown }).code) : 'VAULT_ERROR';
    return {
      value,
      error: {
        content: [{ type: 'text', text: `Error: ${code}: ${error instanceof Error ? error.message : String(error)}` }],
        isError: true,
        errorReason: code,
      } as MCPResult,
    };
  }
}

function maskVaultText(text: string, vault?: { token?: string; plaintext?: string }): string {
  if (!vault?.token || !vault.plaintext) return text;
  return text.split(vault.plaintext).join(vault.token);
}

const definition: MCPToolDefinition = {
  name: 'form_input',
  description: 'Set one form element value by ref. Pass intent="..." (≤120 chars) to label this action in audit logs.\n\nWhen to use: Filling a single known input, textarea, select, or checkbox by ref.\nWhen NOT to use: Use fill_form({fields:{...}}) for multiple fields or optional submit.',
  inputSchema: {
    type: 'object',
    properties: {
      tabId: {
        type: 'string',
        description: 'Tab ID to set form value in',
      },
      ref: {
        type: 'string',
        description: 'Element ref or backendNodeId',
      },
      value: {
        type: 'string',
        description: 'Value to set. Checkboxes: "true"/"false"',
      },
      intent: {
        type: 'string',
        description: 'Human-readable label for this action in audit logs (≤120 chars)',
        maxLength: 120,
      },
      capture_artifact: {
        type: 'boolean',
        default: false,
        description: 'When true, stage a replay artifact step for oc_skill_record. Default false is a strict no-op.',
      },
      returnAfterState: RETURN_AFTER_STATE_SCHEMA,
    },
    required: ['ref', 'value', 'tabId'],
  },
  annotations: TOOL_ANNOTATIONS.form_input,
};

const coreHandler: ToolHandler = async (
  sessionId: string,
  args: Record<string, unknown>,
  context?: ToolContext
): Promise<MCPResult> => {
  const tabId = args.tabId as string;
  const ref = args.ref as string;
  const value = args.value;
  const intent = args.intent as string | undefined;
  const captureArtifact = shouldCaptureReplayArtifact(args.capture_artifact);

  // Validate intent when provided — use typeof guard for null-safety
  if (typeof intent === 'string') {
    if (intent === '') {
      return {
        content: [{ type: 'text', text: 'INVALID_INTENT: intent must not be an empty string' }],
        isError: true,
      };
    }
    if (intent.length > 120) {
      return {
        content: [{ type: 'text', text: `INVALID_INTENT: intent exceeds 120 characters (got ${intent.length})` }],
        isError: true,
      };
    }
  }

  const sessionManager = getSessionManager();
  const refIdManager = getRefIdManager();

  if (!tabId) {
    return {
      content: [{ type: 'text', text: 'Error: tabId is required' }],
      isError: true,
    };
  }

  if (!ref) {
    return {
      content: [{ type: 'text', text: 'Error: ref is required' }],
      isError: true,
    };
  }

  if (value === undefined) {
    return {
      content: [{ type: 'text', text: 'Error: value is required' }],
      isError: true,
    };
  }

  const vaultValue = await resolveMaybeVault(value);
  if (vaultValue.error) return vaultValue.error;
  const inputValue = vaultValue.value;

  // Budget gate: form_input involves multiple sequential CDP calls (resolve + focus + evaluate + type)
  if (context && !hasBudget(context, 10_000)) {
    return {
      content: [{ type: 'text', text: 'form_input: skipped (deadline approaching)' }],
      isError: true,
    };
  }

  try {
    const page = await sessionManager.getPage(sessionId, tabId, undefined, 'form_input');
    if (!page) {
      return {
        content: [{ type: 'text', text: `Error: Tab ${tabId} not found` }],
        isError: true,
      };
    }

    // Get the backend node ID
    let backendNodeId = refIdManager.resolveToBackendNodeId(sessionId, tabId, ref);

    // #831: for `ref_N`-formatted refs (the canonical snapshot output), an
    // unresolvable or stale ref → STALE_REF. Raw integer / `node_N` formats
    // are legacy backend-node-id passthroughs and keep their original error.
    const isRefIdFormat = /^(?:ref_\d+|@e\d+)$/.test(ref);
    if (isRefIdFormat) {
      const entry = refIdManager.getRef(sessionId, tabId, ref);
      if (!entry || refIdManager.isRefStale(sessionId, tabId, ref)) {
        return {
          content: [{ type: 'text', text: formatStaleRefError(ref) }],
          isError: true,
          error: makeStaleRefError(ref),
        };
      }
    } else if (backendNodeId === undefined) {
      return {
        content: [
          {
            type: 'text',
            text: `Error: Reference '${ref}' not found. Use read_page first to get element references (ref_N or backendNodeId).`,
          },
        ],
        isError: true,
      };
    }

    const cdpClient = sessionManager.getCDPClient();

    // Validate ref identity if fingerprint is available.
    //
    // #831: the validateRef check and the STALE_REF dispatch live OUTSIDE
    // the try block — a silent CDP failure must NOT downgrade a ref_N to
    // the legacy "not editable" path. If `DOM.describeNode` rejects we
    // simply skip the fingerprint check and let `DOM.resolveNode` below
    // surface the real error.
    const refEntry = refIdManager.getRef(sessionId, tabId, ref);
    if (refEntry && refEntry.tagName) {
      let nodeLocalName: string | undefined;
      try {
        const { node } = await cdpClient.send<{
          node: { localName: string };
        }>(page, 'DOM.describeNode', { backendNodeId });
        nodeLocalName = node?.localName;
      } catch {
        // Probe failed (element removed, frame detached, etc). Leave
        // nodeLocalName undefined so the validation block below is skipped.
      }

      if (nodeLocalName !== undefined) {
        const validation = refIdManager.validateRef(
          sessionId, tabId, ref,
          nodeLocalName,
        );

        if (!validation.valid && validation.stale) {
          // #831: explicit `ref_N` refs must NOT silently relocate. The
          // snapshot-refs contract requires a STALE_REF so the caller knows
          // to re-snapshot. Legacy raw-id refs keep transparent recovery.
          if (isRefIdFormat) {
            return {
              content: [{ type: 'text', text: formatStaleRefError(ref) }],
              isError: true,
              error: makeStaleRefError(ref),
            };
          }
          // Attempt transparent recovery: re-find the element using stored metadata
          const relocated = await refIdManager.tryRelocateRef(
            sessionId, tabId, ref, page, cdpClient
          );

          if (relocated) {
            console.error(`[ref-recovery] ${ref} was stale, re-located as ${relocated.newRef}`);
            backendNodeId = relocated.backendNodeId;
          } else {
            return {
              content: [{
                type: 'text',
                text: `Error: ${ref} is stale — ${validation.reason}. Element could not be re-located. Run find or read_page again to get fresh refs.`,
              }],
              isError: true,
            };
          }
        }
      }
    }

    // Resolve the node
    const { object } = await cdpClient.send<{ object: { objectId: string } }>(
      page,
      'DOM.resolveNode',
      { backendNodeId }
    );

    if (!object?.objectId) {
      return {
        content: [
          {
            type: 'text',
            text: `Error: Could not resolve element ${ref}. The element may no longer exist.`,
          },
        ],
        isError: true,
      };
    }

    // Detect element type, check disabled/readOnly, and decide approach
    const { result: elementInfo } = await cdpClient.send<{
      result: { value: { tagName: string; type: string; disabled: boolean; readOnly: boolean; contentEditable: boolean } };
    }>(page, 'Runtime.callFunctionOn', {
      objectId: object.objectId,
      functionDeclaration: `
        function() {
          const el = this;
          return {
            tagName: el.tagName.toLowerCase(),
            type: (el.type || '').toLowerCase(),
            disabled: !!el.disabled,
            readOnly: !!el.readOnly,
            contentEditable: el.contentEditable === 'true',
          };
        }
      `,
      returnByValue: true,
    });

    const elInfo = elementInfo.value;

    // Guard: disabled elements
    if (elInfo.disabled) {
      return {
        content: [{
          type: 'text',
          text: `Error: Element is disabled. Enable it first or use javascript_tool to remove the disabled attribute.`,
        }],
        isError: true,
      };
    }

    // Guard: readOnly elements (only for input/textarea)
    if (elInfo.readOnly && (elInfo.tagName === 'input' || elInfo.tagName === 'textarea')) {
      return {
        content: [{
          type: 'text',
          text: `Error: Element is readOnly. Use fill_form with keyboard input or javascript_tool to modify the value programmatically.`,
        }],
        isError: true,
      };
    }

    // Set the value based on element type with DOM delta capture
    const { result, delta } = await withDomDelta(page, async () => {
      if (elInfo.tagName === 'input' || elInfo.tagName === 'textarea') {
        if (elInfo.type === 'checkbox' || elInfo.type === 'radio') {
          // Checkboxes and radios: use injected function (no CDP keyboard needed)
          return cdpClient.send<{
            result: { value: { success: boolean; message?: string; error?: string } };
          }>(page, 'Runtime.callFunctionOn', {
            objectId: object.objectId,
            functionDeclaration: `
              function(newValue) {
                try {
                  const el = this;
                  const shouldCheck = typeof newValue === 'boolean' ? newValue : newValue === 'true' || newValue === true;
                  if (el.checked !== shouldCheck) {
                    el.checked = shouldCheck;
                    el.dispatchEvent(new Event('change', { bubbles: true }));
                  }
                  return { success: true, message: 'Set to ' + shouldCheck };
                } catch (e) {
                  return { success: false, error: e.message };
                }
              }
            `,
            arguments: [{ value: inputValue }],
            returnByValue: true,
          });
        }

        // Text inputs and textareas: try CDP native input first for React/framework compat
        let usedCDP = false;
        try {
          // Focus the element via CDP
          await cdpClient.send(page, 'DOM.focus', { backendNodeId });

          // Select all existing text (Ctrl+A / Cmd+A)
          const selectAllModifier = process.platform === 'darwin' ? 4 : 2; // 4=Meta, 2=Control
          await cdpClient.send(page, 'Input.dispatchKeyEvent', {
            type: 'keyDown',
            key: 'a',
            code: 'KeyA',
            modifiers: selectAllModifier,
          });
          await cdpClient.send(page, 'Input.dispatchKeyEvent', {
            type: 'keyUp',
            key: 'a',
            code: 'KeyA',
          });

          // Insert the new value via CDP Input.insertText
          await cdpClient.send(page, 'Input.insertText', { text: String(inputValue) });
          usedCDP = true;
        } catch {
          // CDP focus/input failed — element may not be focusable via CDP
          usedCDP = false;
        }

        if (usedCDP) {
          return {
            result: {
              value: { success: true, message: `Set value to "${inputValue}"` },
            },
          } as { result: { value: { success: boolean; message?: string; error?: string } } };
        }

        // Fallback: programmatic setter with upgraded InputEvent for better framework compat
        return cdpClient.send<{
          result: { value: { success: boolean; message?: string; error?: string } };
        }>(page, 'Runtime.callFunctionOn', {
          objectId: object.objectId,
          functionDeclaration: `
            function(newValue) {
              try {
                const el = this;
                el.focus();
                const nativeSetter = Object.getOwnPropertyDescriptor(
                  window.HTMLInputElement.prototype, 'value'
                )?.set || Object.getOwnPropertyDescriptor(
                  window.HTMLTextAreaElement.prototype, 'value'
                )?.set;
                if (nativeSetter) {
                  nativeSetter.call(el, String(newValue));
                } else {
                  el.value = String(newValue);
                }
                el.dispatchEvent(new InputEvent('input', {
                  bubbles: true,
                  composed: true,
                  inputType: 'insertText',
                  data: String(newValue),
                }));
                el.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
                return { success: true, message: 'Set value to "' + newValue + '"' };
              } catch (e) {
                return { success: false, error: e.message };
              }
            }
          `,
          arguments: [{ value: inputValue }],
          returnByValue: true,
        });
      } else if (elInfo.tagName === 'select') {
        // Select elements: use native setter for React/framework compat
        return cdpClient.send<{
          result: { value: { success: boolean; message?: string; error?: string } };
        }>(page, 'Runtime.callFunctionOn', {
          objectId: object.objectId,
          functionDeclaration: `
            function(newValue) {
              try {
                const el = this;
                const options = Array.from(el.options);
                const option = options.find(o =>
                  o.value === String(newValue) ||
                  o.textContent?.trim() === String(newValue)
                );
                if (option) {
                  const selectSetter = Object.getOwnPropertyDescriptor(
                    window.HTMLSelectElement.prototype, 'value'
                  )?.set;
                  if (selectSetter) {
                    selectSetter.call(el, option.value);
                  } else {
                    el.value = option.value;
                  }
                  el.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
                  return { success: true, message: 'Selected "' + option.textContent + '"' };
                } else {
                  return { success: false, error: 'Option not found: ' + newValue };
                }
              } catch (e) {
                return { success: false, error: e.message };
              }
            }
          `,
          arguments: [{ value: inputValue }],
          returnByValue: true,
        });
      } else if (elInfo.contentEditable) {
        // Contenteditable elements
        return cdpClient.send<{
          result: { value: { success: boolean; message?: string; error?: string } };
        }>(page, 'Runtime.callFunctionOn', {
          objectId: object.objectId,
          functionDeclaration: `
            function(newValue) {
              try {
                const el = this;
                el.focus();
                el.textContent = String(newValue);
                el.dispatchEvent(new Event('input', { bubbles: true }));
                return { success: true, message: 'Set content to "' + newValue + '"' };
              } catch (e) {
                return { success: false, error: e.message };
              }
            }
          `,
          arguments: [{ value: inputValue }],
          returnByValue: true,
        });
      } else {
        return {
          result: {
            value: { success: false, error: `Element is not editable: ${elInfo.tagName}` },
          },
        } as { result: { value: { success: boolean; message?: string; error?: string } } };
      }
    });

    const response = result.result.value;

    if (response.success) {
      if (captureArtifact) {
        await captureElementReplayStep({
          cdpClient,
          page,
          objectId: object.objectId,
          kind: elInfo.tagName === 'select' ? 'select' : 'fill',
          args: { value: String(value) },
        });
      }
      return {
        content: [{ type: 'text', text: maskVaultText((response.message || 'Value set successfully') + delta, vaultValue) }],
      };
    } else {
      return {
        content: [
          {
            type: 'text',
            text: `Error: ${response.error || 'Failed to set value'}`,
          },
        ],
        isError: true,
      };
    }
  } catch (error) {
    return {
      content: [
        {
          type: 'text',
          text: `Form input error: ${error instanceof Error ? error.message : String(error)}`,
        },
      ],
      isError: true,
    };
  }
};


const handler: ToolHandler = async (sessionId, args, context): Promise<MCPResult> => {
  const result = await coreHandler(sessionId, args, context);
  const returnAfterState = parseReturnAfterState(args.returnAfterState);
  if (returnAfterState === 'none' || result.isError) return result;

  const tabId = args.tabId as string | undefined;
  if (!tabId) return result;

  try {
    const page = await getSessionManager().getPage(sessionId, tabId, undefined, 'form_input');
    if (page) {
      await appendReturnAfterState(result, page, sessionId, tabId, returnAfterState, context);
    }
  } catch {
    // Snapshot chaining is best-effort; never mask the successful action result.
  }
  return result;
};

export function registerFormInputTool(server: MCPServer): void {
  server.registerTool('form_input', handler, definition);
}
