import type { VNode } from 'vue';
import type { DropdownButtonItemArgs } from '../dropdown-button/dropdown-button-item';
import type { FormItemWrapperArgs, MarginType } from '../form/form-item-wrapper';
import { Prop, toNative } from 'vue-facing-decorator';
import PowerduckState from '../../app/powerduck-state';
import TsxComponent, { Component } from '../../app/vuetsx';
import { PortalUtils } from '../../common/utils/utils';
import FormItemWrapper from '../form/form-item-wrapper';

interface TextBoxArgs extends Omit<FormItemWrapperArgs, 'errorId'> {
	value: string;
	placeholder?: string;
	textType?: TextBoxTextType;
	disabled?: boolean;
	autoCompleteText?: string;
	autoCompleteEnabled?: boolean;
	readOnly?: boolean;
	changed: (newValue: string) => void;
	maxLength?: number;
	updateMode?: 'input' | 'change';
	nameAttr?: string;
	keyUp?: (e: KeyboardEvent) => void;
	keyDown?: (e: KeyboardEvent) => void;
	enterPressed?: (e: KeyboardEvent) => void;
	hidePasswordToggle?: boolean;
	/** FloatingField parity: `inputmode` hint (e.g. 'email', 'tel', 'numeric'). */
	inputMode?: string;
	/** FloatingField parity: per-keystroke value transform (mask). Receives raw + previous, returns kept value. */
	mask?: (raw: string, previous: string) => string;
	/** FloatingField parity: monospace value (OTP / codes). */
	mono?: boolean;
}

export const enum TextBoxTextType {
	Text = 'text',
	Password = 'password',
	/**
	 * Same DOM rendering as Password (type="password" — characters hidden), but
	 * sets autocomplete="new-password" by default. Use for password fields that
	 * should NOT trigger the browser's login-form heuristic — admin config fields
	 * for third-party integration credentials, "set new password" forms, etc.
	 *
	 * The login form intentionally uses Password (with explicit autoCompleteText
	 * "current-password") so the browser still autofills saved credentials.
	 */
	PasswordNew = 'password-new',
	Url = 'url',
	Email = 'email',
	Time = 'time',
	Phone = 'phone',
}

@Component
class TextBoxComponent extends TsxComponent<TextBoxArgs> implements TextBoxArgs {
	@Prop() label!: string | VNode;
	@Prop() labelButtons!: DropdownButtonItemArgs[];
	@Prop() subtitle!: string;
	@Prop() value!: string;
	@Prop() placeholder!: string;
	@Prop() cssClass!: string;
	@Prop() mandatory!: boolean;
	@Prop() disabled!: boolean;
	@Prop() wrap!: boolean;
	@Prop() hint: string;
	@Prop() appendIcon: string;
	@Prop() prependIcon: string;
	@Prop() maxWidth?: number;
	@Prop() nameAttr?: string;
	@Prop() marginType?: MarginType;
	@Prop() textType: TextBoxTextType;
	@Prop() readOnly?: boolean;
	@Prop() autoCompleteText?: string;
	@Prop() autoCompleteEnabled: boolean;
	@Prop() appendClicked: () => void;
	@Prop() prependClicked: () => void;
	@Prop() prependIconClicked: () => void;
	@Prop() appendIconClicked: () => void;
	@Prop() keyDown: (e: KeyboardEvent) => void;
	@Prop() keyUp: (e: KeyboardEvent) => void;
	@Prop() enterPressed: (e: KeyboardEvent) => void;
	@Prop() changed: (newValue: string) => void;
	@Prop() showClearValueButton!: boolean;
	@Prop() updateMode?: 'input' | 'change';
	@Prop() maxLength: number;
	@Prop() hidePasswordToggle?: boolean;
	@Prop() inputMode?: string;
	@Prop() mask?: (raw: string, previous: string) => string;
	@Prop() mono?: boolean;

	isPasswordVisible: boolean = false;

	// Defer the "real" autocomplete token until the user actually focuses the field
	// (see getAutoCompleteText). Flips to true on first focus and stays true.
	autoCompleteActivated: boolean = false;

	mounted() {
		if (this.keyDown != null || this.enterPressed != null) {
			this.$el.querySelector('input').addEventListener('keydown', (e) => {
				this.keyDownHandler(e);
			});
		}

		if (this.keyUp != null) {
			this.$el.querySelector('input').addEventListener('keyup', (e) => {
				this.keyUpHandler(e);
			});
		}
	}

	raiseChangeEvent(e) {
		this.populateValidationDeclaration();

		if (this.changed != null) {
			const newValue = e.target.value;
			this.changed(newValue);
		}
	}

	getTypeAttribute(): string {
		if (this.textType === TextBoxTextType.Password && this.isPasswordVisible) {
			return 'text';
		}

		if (this.textType === TextBoxTextType.PasswordNew && this.isPasswordVisible) {
			return 'text';
		}

		if (this.textType === TextBoxTextType.PasswordNew) {
			return 'password';
		}

		return this.textType || 'text';
	}

	togglePasswordVisibility() {
		this.isPasswordVisible = !this.isPasswordVisible;
	}

	get computedAppendIcon(): string {
		if ((this.textType === TextBoxTextType.Password || this.textType === TextBoxTextType.PasswordNew) && !this.hidePasswordToggle) {
			return this.isPasswordVisible ? 'fa fa-eye-slash' : 'fa fa-eye';
		}

		return this.appendIcon;
	}

	get computedAppendIconClicked(): (() => void) {
		if ((this.textType === TextBoxTextType.Password || this.textType === TextBoxTextType.PasswordNew) && !this.hidePasswordToggle) {
			return () => this.togglePasswordVisibility();
		}

		return this.appendIconClicked;
	}

	// Autocomplete is emitted in two phases so a browser can't offer autofill for a recognized
	// login field (username / email / current-password) purely because a modal took focus on
	// open — the behavior Safari exhibits (Chrome only offers on a real user gesture). Until the
	// user focuses the field we emit a suppressing token; on first focus (`activateAutoComplete`)
	// we restore the real token so password managers still pair and save on submit. Nothing here
	// auto-focuses, so the first focus is always user-initiated.
	getAutoCompleteText(): string {
		if (this.autoCompleteText) {
			return this.autoCompleteActivated ? this.autoCompleteText : this.getSuppressedAutoCompleteText();
		}

		if (this.autoCompleteEnabled == false) {
			return this.getSuppressedAutoCompleteText();
		}

		if (this.textType === TextBoxTextType.PasswordNew) {
			return 'new-password';
		}

		return null;
	}

	// Browsers ignore autocomplete="off" for login fields, so on Chrome we use "new-password"
	// (which Chrome honors as "don't autofill saved logins"); on other engines "off" is enough
	// to keep the on-open AutoFill suggestion from appearing.
	private getSuppressedAutoCompleteText(): string {
		return PortalUtils.isBrowserChrome() ? 'new-password' : 'off';
	}

	private activateAutoComplete(e: FocusEvent): void {
		if (this.autoCompleteActivated) {
			return;
		}

		this.autoCompleteActivated = true;

		// Vue re-renders asynchronously; set the attribute on the element synchronously too so the
		// real token is present during THIS focus and the password manager can offer autofill on
		// the first click (not only the next one).
		const el = e.target as HTMLInputElement;
		const token = this.getAutoCompleteText();

		if (token != null) {
			el.setAttribute('autocomplete', token);
		} else {
			el.removeAttribute('autocomplete');
		}
	}

	keyDownHandler(e: KeyboardEvent) {
		if (this.keyDown != null) {
			this.keyDown(e);
		}

		if (this.enterPressed != null && (e.keyCode == 13 || e.which == 13)) {
			this.raiseChangeEvent(e);
			this.enterPressed(e);
			e.preventDefault();
			e.stopPropagation();
			e.stopImmediatePropagation();
		}
	}

	keyUpHandler(e: KeyboardEvent) {
		if (this.keyUp != null) {
			this.keyUp(e);
		}

		if (this.enterPressed != null && (e.keyCode == 13 || e.which == 13)) {
			this.raiseChangeEvent(e);
			this.enterPressed(e);
			e.preventDefault();
			e.stopPropagation();
			e.stopImmediatePropagation();
		}
	}

	// FloatingField parity: optional per-keystroke input mask. When set, transform the raw
	// value, force-sync the DOM (so rejected chars vanish even when the masked result equals
	// the bound value), then emit the masked value on every keystroke.
	private handleInput(e: Event): void {
		const el = e.target as HTMLInputElement;

		if (this.mask != null) {
			const masked = this.mask(el.value, this.value ?? '');

			if (el.value !== masked) {
				el.value = masked;
			}

			this.populateValidationDeclaration();
			this.changed?.(masked);

			return;
		}

		if (this.updateMode == 'input') {
			this.raiseChangeEvent(e);
		}
	}

	private get errorId(): string {
		return `textbox-error-${this.$.uid}`;
	}

	render(h) {
		const hasError = this.hasValidationError;

		return (
			<FormItemWrapper label={this.label} cssClass={this.cssClass} mandatory={this.mandatory} wrap={this.wrap} appendIcon={this.computedAppendIcon} prependIcon={this.prependIcon} hint={this.hint} marginType={this.marginType} appendClicked={this.appendClicked} prependClicked={this.prependClicked} prependIconClicked={this.prependIconClicked} appendIconClicked={this.computedAppendIconClicked} maxWidth={this.maxWidth} validationState={this.validationState} labelButtons={this.labelButtons} subtitle={this.subtitle} showClearValueButton={this.showClearValueButton} errorId={this.errorId}>
				<input
					type={this.getTypeAttribute()}
					disabled={this.disabled as any != true ? null : 'disabled'}
					readonly={this.readOnly as any != true ? null : 'readonly'}
					value={this.value}
					maxlength={this.maxLength}
					name={this.nameAttr}
					onChange={e => this.raiseChangeEvent(e)}
					onInput={e => this.handleInput(e)}
					onFocus={e => this.activateAutoComplete(e)}
					class={this.mono === true ? `${PowerduckState.getFormControlCssClass()} is-mono` : PowerduckState.getFormControlCssClass()}
					inputmode={this.inputMode}
					placeholder={this.placeholder}
					autocomplete={this.getAutoCompleteText()}
					aria-invalid={hasError ? 'true' : undefined}
					aria-describedby={hasError ? this.errorId : undefined}
					aria-required={this.mandatory ? 'true' : undefined}
				/>
			</FormItemWrapper>
		);
	}
}

const TextBox = toNative(TextBoxComponent);
export default TextBox;
