import { useEffect, useMemo, useState, type ReactNode } from "react";
import { Match, Option } from "effect";

import { Checkbox } from "./checkbox";
import { Input } from "./input";
import { Label } from "./label";
import { NativeSelect, NativeSelectOption } from "./native-select";
import { Textarea } from "./textarea";

export type ElicitationAction = "accept" | "decline" | "cancel";
export type ElicitationFieldValue = string | number | boolean | string[];

type ElicitationSchemaField = {
  readonly name: string;
  readonly schema: Record<string, unknown>;
  readonly required: boolean;
};

type ElicitationFormSchema = {
  readonly fields: readonly ElicitationSchemaField[];
};

type SelectOption = {
  readonly value: string;
  readonly label: string;
};

export type ElicitationApprovalState = {
  readonly hasFields: boolean;
  readonly content: () => Record<string, unknown> | null;
  readonly fields: ReactNode;
};

const isRecord = (value: unknown): value is Record<string, unknown> =>
  typeof value === "object" && value !== null && !Array.isArray(value);

const isStringArray = (value: unknown): value is string[] =>
  Array.isArray(value) && value.every((item) => typeof item === "string");

const toOptionalString = (value: unknown): string | undefined =>
  typeof value === "string" && value.length > 0 ? value : undefined;

const fieldLabel = (field: ElicitationSchemaField): string =>
  toOptionalString(field.schema.title) ?? field.name;

const fieldDescription = (field: ElicitationSchemaField): string | undefined =>
  toOptionalString(field.schema.description);

const enumOptions = (schema: Record<string, unknown>): readonly SelectOption[] => {
  const oneOf = schema.oneOf;
  if (Array.isArray(oneOf)) {
    return oneOf.flatMap((item): SelectOption[] => {
      if (!isRecord(item) || typeof item.const !== "string") return [];
      return [{ value: item.const, label: toOptionalString(item.title) ?? item.const }];
    });
  }

  const values = schema.enum;
  if (!isStringArray(values)) return [];
  const labels = isStringArray(schema.enumNames) ? schema.enumNames : values;
  return values.map((value, index) => ({ value, label: labels[index] ?? value }));
};

const multiSelectOptions = (schema: Record<string, unknown>): readonly SelectOption[] => {
  const items = schema.items;
  if (!isRecord(items)) return [];

  const anyOf = items.anyOf;
  if (Array.isArray(anyOf)) {
    return anyOf.flatMap((item): SelectOption[] => {
      if (!isRecord(item) || typeof item.const !== "string") return [];
      return [{ value: item.const, label: toOptionalString(item.title) ?? item.const }];
    });
  }

  const values = items.enum;
  if (!isStringArray(values)) return [];
  const labels = isStringArray(items.enumNames) ? items.enumNames : values;
  return values.map((value, index) => ({ value, label: labels[index] ?? value }));
};

const parseElicitationFormSchema = (value: unknown): ElicitationFormSchema | null => {
  if (!isRecord(value) || !isRecord(value.properties)) return null;
  const required = isStringArray(value.required) ? value.required : [];
  const fields = Object.entries(value.properties).flatMap(
    ([name, schema]): ElicitationSchemaField[] =>
      isRecord(schema) ? [{ name, schema, required: required.includes(name) }] : [],
  );
  return fields.length > 0 ? { fields } : null;
};

const initialFieldValue = (field: ElicitationSchemaField): ElicitationFieldValue | undefined => {
  const value = field.schema.default;
  if (value === undefined) return undefined;
  const matched = Match.value(field.schema.type).pipe(
    Match.when("boolean", () => value === true),
    Match.when(
      (type: unknown) => type === "number" || type === "integer",
      () => {
        const numberValue = typeof value === "number" ? value : Number(value);
        return Number.isFinite(numberValue) ? numberValue : undefined;
      },
    ),
    Match.when("array", () => (isStringArray(value) ? value : undefined)),
    Match.option,
  );
  return Option.getOrElse(matched, () => (typeof value === "string" ? value : String(value)));
};

const initialFormValues = (
  formSchema: ElicitationFormSchema | null,
): Record<string, ElicitationFieldValue> => {
  const values: Record<string, ElicitationFieldValue> = {};
  for (const field of formSchema?.fields ?? []) {
    const value = initialFieldValue(field);
    if (value !== undefined) values[field.name] = value;
  }
  return values;
};

const isEmptyFormValue = (value: ElicitationFieldValue | undefined): boolean =>
  value === undefined || value === "" || (Array.isArray(value) && value.length === 0);

const numericConstraint = (
  schema: Record<string, unknown>,
  key: "minimum" | "maximum",
): number | undefined => (typeof schema[key] === "number" ? schema[key] : undefined);

const lengthConstraint = (
  schema: Record<string, unknown>,
  key: "minLength" | "maxLength" | "minItems" | "maxItems",
): number | undefined => (typeof schema[key] === "number" ? schema[key] : undefined);

const validateEmail = (value: string): boolean => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);

const validateUrl = (value: string): boolean => {
  try {
    void new URL(value);
    return true;
  } catch {
    return false;
  }
};

const validateFieldValue = (
  field: ElicitationSchemaField,
  value: ElicitationFieldValue | undefined,
): { readonly value?: ElicitationFieldValue; readonly error?: string } => {
  if (isEmptyFormValue(value)) {
    return field.required ? { error: "This field is required." } : {};
  }

  if (field.schema.type === "boolean") {
    return typeof value === "boolean" ? { value } : { error: "Choose true or false." };
  }

  if (field.schema.type === "number" || field.schema.type === "integer") {
    const numberValue = typeof value === "number" ? value : Number(value);
    const typeLabel = field.schema.type === "integer" ? "an integer" : "a number";
    if (!Number.isFinite(numberValue)) return { error: `Must be ${typeLabel}.` };
    if (field.schema.type === "integer" && !Number.isInteger(numberValue)) {
      return { error: "Must be an integer." };
    }
    const minimum = numericConstraint(field.schema, "minimum");
    const maximum = numericConstraint(field.schema, "maximum");
    if (minimum !== undefined && numberValue < minimum) return { error: `Must be >= ${minimum}.` };
    if (maximum !== undefined && numberValue > maximum) return { error: `Must be <= ${maximum}.` };
    return { value: numberValue };
  }

  if (field.schema.type === "array") {
    const selected = Array.isArray(value) ? value : [];
    const options = multiSelectOptions(field.schema);
    const allowed = new Set(options.map((option) => option.value));
    if (!selected.every((item) => allowed.has(item))) return { error: "Choose a valid option." };
    const minItems = lengthConstraint(field.schema, "minItems");
    const maxItems = lengthConstraint(field.schema, "maxItems");
    if (minItems !== undefined && selected.length < minItems) {
      return { error: `Choose at least ${minItems} option${minItems === 1 ? "" : "s"}.` };
    }
    if (maxItems !== undefined && selected.length > maxItems) {
      return { error: `Choose at most ${maxItems} option${maxItems === 1 ? "" : "s"}.` };
    }
    return { value: selected };
  }

  const stringValue = String(value);
  const options = enumOptions(field.schema);
  if (options.length > 0 && !options.some((option) => option.value === stringValue)) {
    return { error: "Choose a valid option." };
  }
  const minLength = lengthConstraint(field.schema, "minLength");
  const maxLength = lengthConstraint(field.schema, "maxLength");
  if (minLength !== undefined && stringValue.length < minLength) {
    return { error: `Must be at least ${minLength} character${minLength === 1 ? "" : "s"}.` };
  }
  if (maxLength !== undefined && stringValue.length > maxLength) {
    return { error: `Must be at most ${maxLength} character${maxLength === 1 ? "" : "s"}.` };
  }
  if (field.schema.format === "email" && !validateEmail(stringValue)) {
    return { error: "Must be a valid email address." };
  }
  if (field.schema.format === "uri" && !validateUrl(stringValue)) {
    return { error: "Must be a valid URL." };
  }
  return { value: stringValue };
};

const buildElicitationContent = (
  formSchema: ElicitationFormSchema | null,
  formValues: Record<string, ElicitationFieldValue>,
): { readonly content: Record<string, unknown>; readonly errors: Record<string, string> } => {
  if (!formSchema) return { content: {}, errors: {} };

  const content: Record<string, unknown> = {};
  const errors: Record<string, string> = {};
  for (const field of formSchema.fields) {
    const result = validateFieldValue(field, formValues[field.name]);
    if (result.error) {
      errors[field.name] = result.error;
      continue;
    }
    if (result.value !== undefined) content[field.name] = result.value;
  }
  return { content, errors };
};

/**
 * The content an approval would send if nobody touched the form: every field's
 * schema default, validated by exactly the same rules the form applies.
 *
 * `null` means "this schema cannot be answered without a human": a required
 * field has no default, or a default fails its own constraints. Callers that
 * approve without showing the form (the shell's remembered "don't ask again"
 * grants) MUST fall back to the form in that case rather than sending
 * fabricated values — a remembered approval is consent to skip the prompt, not
 * consent to invent the answer.
 */
export const elicitationDefaultContent = (schema: unknown): Record<string, unknown> | null => {
  const formSchema = parseElicitationFormSchema(schema);
  const result = buildElicitationContent(formSchema, initialFormValues(formSchema));
  return Object.keys(result.errors).length > 0 ? null : result.content;
};

export function useElicitationApproval(schema: unknown): ElicitationApprovalState {
  const formSchema = useMemo(() => parseElicitationFormSchema(schema), [schema]);
  const [formValues, setFormValues] = useState<Record<string, ElicitationFieldValue>>(() =>
    initialFormValues(formSchema),
  );
  const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});

  useEffect(() => {
    setFormValues(initialFormValues(formSchema));
    setFieldErrors({});
  }, [formSchema]);

  const setFieldValue = (name: string, value: ElicitationFieldValue | undefined) => {
    setFormValues((prev) => {
      const next = { ...prev };
      if (value === undefined) {
        delete next[name];
      } else {
        next[name] = value;
      }
      return next;
    });
    setFieldErrors((prev) => {
      if (!prev[name]) return prev;
      const next = { ...prev };
      delete next[name];
      return next;
    });
  };

  return {
    hasFields: Boolean(formSchema),
    content: () => {
      const result = buildElicitationContent(formSchema, formValues);
      setFieldErrors(result.errors);
      return Object.keys(result.errors).length > 0 ? null : result.content;
    },
    fields: formSchema ? (
      <ElicitationApprovalFields
        formSchema={formSchema}
        formValues={formValues}
        fieldErrors={fieldErrors}
        onChange={setFieldValue}
      />
    ) : null,
  };
}

function ElicitationApprovalFields({
  formSchema,
  formValues,
  fieldErrors,
  onChange,
}: {
  formSchema: ElicitationFormSchema;
  formValues: Record<string, ElicitationFieldValue>;
  fieldErrors: Record<string, string>;
  onChange: (name: string, value: ElicitationFieldValue | undefined) => void;
}) {
  return (
    <div data-testid="trusted-interaction-fields" className="space-y-3">
      <div>
        <div className="text-xs font-medium text-muted-foreground">Additional details</div>
        <div className="mt-0.5 text-xs text-muted-foreground">
          These values will be sent with approval.
        </div>
      </div>
      {formSchema.fields.map((field) => (
        <ElicitationApprovalField
          key={field.name}
          field={field}
          value={formValues[field.name]}
          error={fieldErrors[field.name]}
          onChange={(value) => onChange(field.name, value)}
        />
      ))}
    </div>
  );
}

function ElicitationApprovalField({
  field,
  value,
  error,
  onChange,
}: {
  field: ElicitationSchemaField;
  value: ElicitationFieldValue | undefined;
  error: string | undefined;
  onChange: (value: ElicitationFieldValue | undefined) => void;
}) {
  const label = fieldLabel(field);
  const description = fieldDescription(field);
  const fieldId = `trusted-interaction-field-${field.name}`;
  const describedBy = description ? `${fieldId}-description` : undefined;
  const errorId = error ? `${fieldId}-error` : undefined;
  const ariaDescribedBy = [describedBy, errorId].filter(Boolean).join(" ") || undefined;

  return (
    <div className="space-y-1.5" data-testid={`trusted-interaction-field-${field.name}`}>
      <Label htmlFor={fieldId} className="text-xs font-medium">
        {label}
        {field.required && <span className="ml-1 text-destructive">*</span>}
      </Label>
      <ElicitationApprovalFieldControl
        field={field}
        fieldId={fieldId}
        value={value}
        ariaDescribedBy={ariaDescribedBy}
        invalid={Boolean(error)}
        onChange={onChange}
      />
      {description && (
        <div id={describedBy} className="text-xs text-muted-foreground">
          {description}
        </div>
      )}
      {error && (
        <div id={errorId} className="text-xs text-destructive">
          {error}
        </div>
      )}
    </div>
  );
}

function ElicitationApprovalFieldControl({
  field,
  fieldId,
  value,
  ariaDescribedBy,
  invalid,
  onChange,
}: {
  field: ElicitationSchemaField;
  fieldId: string;
  value: ElicitationFieldValue | undefined;
  ariaDescribedBy: string | undefined;
  invalid: boolean;
  onChange: (value: ElicitationFieldValue | undefined) => void;
}) {
  const options = enumOptions(field.schema);
  if (options.length > 0) {
    return (
      <NativeSelect
        id={fieldId}
        value={typeof value === "string" ? value : ""}
        onChange={(event) => onChange(event.target.value || undefined)}
        aria-describedby={ariaDescribedBy}
        aria-invalid={invalid}
        className="w-full"
      >
        <NativeSelectOption value="">Select...</NativeSelectOption>
        {options.map((option) => (
          <NativeSelectOption key={option.value} value={option.value}>
            {option.label}
          </NativeSelectOption>
        ))}
      </NativeSelect>
    );
  }

  const multiOptions = multiSelectOptions(field.schema);
  if (multiOptions.length > 0) {
    const selected = Array.isArray(value) ? value : [];
    return (
      <div
        id={fieldId}
        aria-describedby={ariaDescribedBy}
        aria-invalid={invalid}
        className="space-y-1.5 rounded-md border border-border p-2"
      >
        {multiOptions.map((option) => {
          const checked = selected.includes(option.value);
          return (
            <Label key={option.value} className="flex items-center gap-2 text-sm">
              <Checkbox
                checked={checked}
                onCheckedChange={(nextChecked) => {
                  const next = nextChecked === true;
                  onChange(
                    next
                      ? [...selected, option.value]
                      : selected.filter((item) => item !== option.value),
                  );
                }}
              />
              <span>{option.label}</span>
            </Label>
          );
        })}
      </div>
    );
  }

  if (field.schema.type === "boolean") {
    return (
      <Label className="flex items-center gap-2 rounded-md border border-border p-2 text-sm">
        <Checkbox
          id={fieldId}
          checked={value === true}
          onCheckedChange={(nextChecked) => onChange(nextChecked === true)}
          aria-describedby={ariaDescribedBy}
          aria-invalid={invalid}
        />
        <span>Yes</span>
      </Label>
    );
  }

  if (field.schema.type === "number" || field.schema.type === "integer") {
    return (
      <Input
        id={fieldId}
        type="number"
        value={value === undefined ? "" : String(value)}
        step={field.schema.type === "integer" ? 1 : "any"}
        min={numericConstraint(field.schema, "minimum")}
        max={numericConstraint(field.schema, "maximum")}
        onChange={(event) => onChange(event.target.value)}
        aria-describedby={ariaDescribedBy}
        aria-invalid={invalid}
      />
    );
  }

  const isLongText = typeof field.schema.maxLength === "number" && field.schema.maxLength > 160;
  if (field.schema.type === "string" && isLongText && field.schema.format === undefined) {
    return (
      <Textarea
        id={fieldId}
        value={typeof value === "string" ? value : ""}
        onChange={(event) => onChange(event.target.value)}
        aria-describedby={ariaDescribedBy}
        aria-invalid={invalid}
        className="min-h-20"
      />
    );
  }

  const inputType =
    field.schema.format === "email" ? "email" : field.schema.format === "uri" ? "url" : "text";
  return (
    <Input
      id={fieldId}
      type={inputType}
      value={typeof value === "string" ? value : ""}
      onChange={(event) => onChange(event.target.value)}
      aria-describedby={ariaDescribedBy}
      aria-invalid={invalid}
    />
  );
}
