import { Component, Prop, Element, h, Watch, State, AttachInternals } from '@stencil/core';

import { Button } from './button.interface';
import { ButtonType, ButtonTypes, HtmlType, HtmlTypes } from './ontario-button.types';

import { validatePropExists, validateValueAgainstArray } from '../../utils/validation/validation-functions';
import { ConsoleMessageClass } from '../../utils/console-message/console-message';
import { isServerSideRendering } from '../../utils/common/environment';

/**
 * Ontario Button triggers actions and supports button or link behavior.
 *
 * This component intentionally does not expose a `disabled` prop.
 *
 * To support accessible and understandable form completion:
 * - keep actions available
 * - use validation and error messaging to guide corrections instead of disabling
 *
 * For component guidance, see:
 * - https://designsystem.ontario.ca/components/detail/buttons.html
 * - https://designsystem.ontario.ca/developer-docs/components/ontario-button/
 *
 * Disabled/read-only policy source:
 * - https://designsystem.ontario.ca/components/detail/buttons.html#disabled-buttons
 */
@Component({
	tag: 'ontario-button',
	styleUrl: 'ontario-button.scss',
	shadow: true,
	formAssociated: true,
})
export class OntarioButton implements Button {
	@Element() host: HTMLElement;
	@AttachInternals() internals: ElementInternals;

	/**
	 * The type of button to render.
	 *
	 * If no type is passed, it will default to 'secondary'.
	 */
	@Prop() type: ButtonType = 'secondary';

	/**
	 * The native HTML button type the button should use.
	 *
	 * If no `htmlType` is passed, it will default to `'button'`.
	 * This prop only affects the component when it renders as a native `<button>`.
	 * If `href` is provided, the component renders as a native `<a>` and `htmlType` is ignored.
	 */
	@Prop() htmlType: HtmlType = 'button';

	/**
	 * When provided, the component renders as a native anchor for navigation use cases.
	 * This takes precedence over `htmlType`, so form-submission behaviour is disabled in link mode.
	 */
	@Prop() href?: string;

	/**
	 * Specifies where to open the linked document when `href` is provided.
	 * This prop has no effect unless the component is in link mode.
	 */
	@Prop() target?: string;

	/**
	 * Specifies the relationship of the linked document to the current document when `href` is provided.
	 * This prop has no effect unless the component is in link mode.
	 */
	@Prop() rel?: string;

	/**
	 * Text to be displayed within the button. This will override the text provided through the host element textContent.
	 *
	 * @example
	 * <ontario-button label="Label Text">Text</ontario-button>
	 *
	 * The resulting button will have the label `"Label Text"`.
	 */
	@Prop() label?: string;

	/**
	 * Provides more context as to what the button interaction is doing. This should only be used for accessibility purposes, if the button interaction requires more description than what the text provides.
	 *
	 *  This is optional.
	 *
	 * @example
	 * <ontario-button aria-label-text="Click button to open map">Open</ontario button>
	 */
	@Prop({ mutable: true }) ariaLabelText?: string;

	/**
	 * The unique identifier of the button. This is optional - if no ID is passed, one will be generated.
	 */
	@Prop({ mutable: true }) elementId?: string;

	/**
	 * Mutable variable, for internal use only.
	 * Set the button's type depending on validation result.
	 */
	@State() private typeState: string;

	/**
	 * Mutable variable, for internal use only.
	 * Set the native HTML button type depending on validation result.
	 */
	@State() private htmlTypeState: string;

	@State() private labelState: string;

	/**
	 * A reference to the internal button element.
	 */

	private buttonRef: HTMLButtonElement;

	private hasWarnedLinkHtmlTypeConflict = false;

	/*
	 * Watch for changes to the `label` property for validation purposes.
	 *
	 * If  no `label` prop is provided, the `label` prop will be set to the host element textContent (if it exists).
	 */
	@Watch('label')
	private updateLabelContent() {
		if (isServerSideRendering()) {
			// Ensure host and its textContent are defined and accessible (safe for SSR environments)
			this.labelState = this.label ?? '';
		} else {
			this.labelState = this.label ?? this.host.textContent ?? '';
		}
		this.validateLabelContent(this.labelState);
	}

	/**
	 * Watch for changes to the `type` property for validation purposes.
	 *
	 * If the user input doesn't match one of the array values then `type` will be set to its default (`secondary`).
	 * If a match is found in one of the array values then `type` will be set to the matching array key value.
	 */
	@Watch('type')
	validateType() {
		const isValid = validateValueAgainstArray(this.type, ButtonTypes);
		this.typeState = isValid ? this.type : this.warnDefaultType();
	}

	/**
	 *  Watch for changes to the `htmlType` property for validation purposes.
	 *
	 * If the user input doesn't match one of the array values then `htmlType` will be set to its default (`submit`).
	 * If a match is found in one of the array values then `htmlType` will be set to the matching array key value.
	 */
	@Watch('htmlType')
	validateHtmlType() {
		const isValid = validateValueAgainstArray(this.htmlType, HtmlTypes);
		this.htmlTypeState = isValid ? this.htmlType : this.warnDefaultHtmlType();
	}

	/**
	 * Print the missing `label` prop warning message
	 */
	validateLabelContent(newValue: string) {
		if (validatePropExists(newValue)) {
			const message = new ConsoleMessageClass();
			message
				.addDesignSystemTag()
				.addMonospaceText(' label ')
				.addRegularText('for')
				.addMonospaceText(' <ontario-button> ')
				.addRegularText('was not provided')
				.printMessage();
		}
	}

	/**
	 * Print the invalid `type` prop warning message
	 * @returns default type (secondary)
	 */
	private warnDefaultType(): ButtonType {
		const message = new ConsoleMessageClass();
		message
			.addDesignSystemTag()
			.addMonospaceText(' type ')
			.addRegularText('on')
			.addMonospaceText(' <ontario-button> ')
			.addRegularText('was set to an invalid type; only')
			.addMonospaceText(' primary, secondary, or tertiary ')
			.addRegularText('are supported. The default type')
			.addMonospaceText(' secondary ')
			.addRegularText('is assumed.')
			.printMessage();
		return 'secondary';
	}

	/**
	 * Print the invalid `htmlType` warning message
	 * @returns default htmlType (button)
	 */
	private warnDefaultHtmlType(): HtmlType {
		const message = new ConsoleMessageClass();
		message
			.addDesignSystemTag()
			.addMonospaceText(' htmlType ')
			.addRegularText('on')
			.addMonospaceText(' <ontario-button> ')
			.addRegularText('was set to an invalid htmlType; only')
			.addMonospaceText(' button, reset, or submit ')
			.addRegularText('are supported. The default type')
			.addMonospaceText(' button ')
			.addRegularText('is assumed.')
			.printMessage();
		return 'button';
	}

	/**
	 * @returns the classes of the button based of the button's `type`.
	 */
	private getClass() {
		return `ontario-button ontario-button--${this.typeState}`;
	}

	private isLinkMode() {
		return !!this.href;
	}

	private warnIgnoredHtmlTypeInLinkMode() {
		if (this.hasWarnedLinkHtmlTypeConflict || !this.isLinkMode() || this.htmlTypeState === 'button') {
			return;
		}

		const message = new ConsoleMessageClass();
		message
			.addDesignSystemTag()
			.addMonospaceText(' htmlType ')
			.addRegularText('on')
			.addMonospaceText(' <ontario-button> ')
			.addRegularText('is ignored when')
			.addMonospaceText(' href ')
			.addRegularText('is provided. The component renders as a native link instead.')
			.printMessage();

		this.hasWarnedLinkHtmlTypeConflict = true;
	}

	public getId(): string {
		return this.elementId ?? '';
	}

	/**
	 * Set `buttonId`, `label`, and `ariaLabel` using internal component logic.
	 */
	componentWillLoad() {
		this.updateLabelContent();
		this.validateHtmlType();
		this.validateType();
		this.ariaLabelText = this.ariaLabelText ?? this.labelState;
	}

	componentWillRender() {
		this.warnIgnoredHtmlTypeInLinkMode();
	}

	componentDidLoad() {
		// Used to help load translations for any slots + text content passed in by the user.
		const observer = new MutationObserver((mutations) => {
			mutations.forEach((mutation) => {
				if (mutation.type === 'attributes') {
					this.updateLabelContent();
				}
			});
		});

		const options = { attributes: true };
		observer.observe(this.host, options);

		// Shadow DOM prevents the internal button's type from interacting with the outer form natively.
		// Use ElementInternals to trigger submit/reset programmatically on click instead.
		if (!this.isLinkMode()) {
			this.buttonRef.addEventListener('click', () => {
				const { form } = this.internals;
				if (!form) return;
				if (this.htmlTypeState === 'submit') {
					form.requestSubmit();
				} else if (this.htmlTypeState === 'reset') {
					form.reset();
				}
			});
		}
	}

	render() {
		if (this.isLinkMode()) {
			return (
				<a
					href={this.href}
					target={this.target}
					rel={this.rel}
					class={this.getClass()}
					aria-label={this.ariaLabelText}
					id={this.getId()}
				>
					{this.labelState}
				</a>
			);
		}

		return (
			<button
				ref={(el) => (this.buttonRef = el as HTMLButtonElement)}
				type={this.htmlTypeState}
				class={this.getClass()}
				aria-label={this.ariaLabelText}
				id={this.getId()}
			>
				{this.labelState}
			</button>
		);
	}
}
