"""Adds config flow for SQL integration."""

import logging
from typing import Any, override

import sqlalchemy
from sqlalchemy.engine import Engine, Result
from sqlalchemy.exc import MultipleResultsFound, NoSuchColumnError, SQLAlchemyError
from sqlalchemy.orm import Session, scoped_session, sessionmaker
import voluptuous as vol

from homeassistant.components.recorder import CONF_DB_URL, get_instance
from homeassistant.components.sensor import (
    CONF_STATE_CLASS,
    SensorDeviceClass,
    SensorStateClass,
)
from homeassistant.config_entries import (
    ConfigEntry,
    ConfigFlow,
    ConfigFlowResult,
    OptionsFlowWithReload,
)
from homeassistant.const import (
    CONF_DEVICE_CLASS,
    CONF_NAME,
    CONF_UNIT_OF_MEASUREMENT,
    CONF_VALUE_TEMPLATE,
)
from homeassistant.core import async_get_hass, callback
from homeassistant.data_entry_flow import section
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import selector

from .const import CONF_ADDITIONAL_OPTIONS, CONF_COLUMN_NAME, CONF_QUERY, DOMAIN
from .util import (
    EmptyQueryError,
    InvalidSqlQuery,
    MultipleQueryError,
    NotSelectQueryError,
    UnknownQueryTypeError,
    check_and_render_sql_query,
    resolve_db_url,
)

_LOGGER = logging.getLogger(__name__)


OPTIONS_SCHEMA: vol.Schema = vol.Schema(
    {
        vol.Required(CONF_QUERY): selector.TemplateSelector(),
        vol.Required(CONF_COLUMN_NAME): selector.TextSelector(),
        vol.Required(CONF_ADDITIONAL_OPTIONS): section(
            vol.Schema(
                {
                    vol.Optional(CONF_VALUE_TEMPLATE): selector.TemplateSelector(),
                    vol.Optional(CONF_UNIT_OF_MEASUREMENT): selector.TextSelector(),
                    vol.Optional(CONF_DEVICE_CLASS): selector.SelectSelector(
                        selector.SelectSelectorConfig(
                            options=[
                                cls.value
                                for cls in SensorDeviceClass
                                if cls != SensorDeviceClass.ENUM
                            ],
                            mode=selector.SelectSelectorMode.DROPDOWN,
                            translation_key="device_class",
                            sort=True,
                        )
                    ),
                    vol.Optional(CONF_STATE_CLASS): selector.SelectSelector(
                        selector.SelectSelectorConfig(
                            options=[cls.value for cls in SensorStateClass],
                            mode=selector.SelectSelectorMode.DROPDOWN,
                            translation_key="state_class",
                            sort=True,
                        )
                    ),
                }
            ),
            {"collapsed": True},
        ),
    }
)

CONFIG_SCHEMA: vol.Schema = vol.Schema(
    {
        # Approved exemption: user names the SQL query sensor
        # pylint: disable-next=home-assistant-config-flow-name-field
        vol.Required(CONF_NAME, default="Select SQL Query"): selector.TextSelector(),
        vol.Optional(CONF_DB_URL): selector.TextSelector(),
    }
)


def validate_sql_select(value: str) -> str:
    """Validate that value is a SQL SELECT query."""
    hass = async_get_hass()
    try:
        return check_and_render_sql_query(hass, value)
    except (TemplateError, InvalidSqlQuery) as err:
        _LOGGER.debug("Invalid query '%s' results in '%s'", value, err.args[0])
        raise


def validate_db_connection(db_url: str) -> bool:
    """Validate db connection."""

    engine: Engine | None = None
    sess: Session | None = None
    try:
        engine = sqlalchemy.create_engine(db_url, future=True)
        sessmaker = scoped_session(sessionmaker(bind=engine, future=True))
        sess = sessmaker()
        sess.execute(sqlalchemy.text("select 1 as value"))
    except SQLAlchemyError as error:
        _LOGGER.debug("Execution error %s", error)
        if sess:
            sess.close()
        if engine:
            engine.dispose()
        raise

    if sess:
        sess.close()
        engine.dispose()

    return True


def validate_query(db_url: str, query: str, column: str) -> bool:
    """Validate SQL query."""

    engine = sqlalchemy.create_engine(db_url, future=True)
    sessmaker = scoped_session(sessionmaker(bind=engine, future=True))
    sess: Session = sessmaker()

    try:
        result: Result = sess.execute(sqlalchemy.text(query))
    except SQLAlchemyError as error:
        _LOGGER.debug("Execution error %s", error)
        if sess:
            sess.close()
            engine.dispose()
        raise InvalidSqlQuery from error

    for res in result.mappings():
        if column not in res:
            _LOGGER.debug("Column `%s` is not returned by the query", column)
            if sess:
                sess.close()
                engine.dispose()
            raise NoSuchColumnError(f"Column {column} is not returned by the query.")

        data = res[column]
        _LOGGER.debug("Return value from query: %s", data)

    if sess:
        sess.close()
        engine.dispose()

    return True


class SQLConfigFlow(ConfigFlow, domain=DOMAIN):
    """Handle a config flow for SQL integration."""

    VERSION = 3

    data: dict[str, Any]

    @staticmethod
    @callback
    @override
    def async_get_options_flow(
        config_entry: ConfigEntry,
    ) -> SQLOptionsFlowHandler:
        """Get the options flow for this handler."""
        return SQLOptionsFlowHandler()

    @override
    async def async_step_user(
        self, user_input: dict[str, Any] | None = None
    ) -> ConfigFlowResult:
        """Handle the user step."""
        errors = {}

        if user_input is not None:
            db_url = user_input.get(CONF_DB_URL)

            try:
                db_url_for_validation = resolve_db_url(self.hass, db_url)
                await self.hass.async_add_executor_job(
                    validate_db_connection, db_url_for_validation
                )
            except SQLAlchemyError:
                errors["db_url"] = "db_url_invalid"

            if not errors:
                self.data = {CONF_NAME: user_input[CONF_NAME]}
                if db_url and db_url_for_validation != get_instance(self.hass).db_url:
                    self.data[CONF_DB_URL] = db_url
                return await self.async_step_options()

        return self.async_show_form(
            step_id="user",
            data_schema=self.add_suggested_values_to_schema(CONFIG_SCHEMA, user_input),
            errors=errors,
        )

    async def async_step_options(
        self, user_input: dict[str, Any] | None = None
    ) -> ConfigFlowResult:
        """Handle the user step."""
        errors = {}
        description_placeholders = {}

        if user_input is not None:
            query = user_input[CONF_QUERY]
            column = user_input[CONF_COLUMN_NAME]

            try:
                query = validate_sql_select(query)
                db_url_for_validation = resolve_db_url(
                    self.hass, self.data.get(CONF_DB_URL)
                )
                await self.hass.async_add_executor_job(
                    validate_query, db_url_for_validation, query, column
                )
            except NoSuchColumnError:
                errors["column"] = "column_invalid"
                description_placeholders = {"column": column}
            except MultipleResultsFound, MultipleQueryError:
                errors["query"] = "multiple_queries"
            except SQLAlchemyError:
                errors["db_url"] = "db_url_invalid"
            except NotSelectQueryError, UnknownQueryTypeError:
                errors["query"] = "query_no_read_only"
            except (TemplateError, EmptyQueryError, InvalidSqlQuery) as err:
                _LOGGER.debug("Invalid query: %s", err)
                errors["query"] = "query_invalid"

            mod_additional_options = {
                k: v
                for k, v in user_input[CONF_ADDITIONAL_OPTIONS].items()
                if v is not None
            }
            user_input[CONF_ADDITIONAL_OPTIONS] = mod_additional_options

            if not errors:
                name = self.data[CONF_NAME]
                self.data.pop(CONF_NAME)
                return self.async_create_entry(
                    title=name,
                    data=self.data,
                    options=user_input,
                )

        return self.async_show_form(
            step_id="options",
            data_schema=self.add_suggested_values_to_schema(OPTIONS_SCHEMA, user_input),
            errors=errors,
            description_placeholders=description_placeholders,
        )


class SQLOptionsFlowHandler(OptionsFlowWithReload):
    """Handle SQL options."""

    async def async_step_init(
        self, user_input: dict[str, Any] | None = None
    ) -> ConfigFlowResult:
        """Manage SQL options."""
        errors = {}
        description_placeholders = {}

        if user_input is not None:
            db_url = self.config_entry.data.get(CONF_DB_URL)
            query = user_input[CONF_QUERY]
            column = user_input[CONF_COLUMN_NAME]

            try:
                query = validate_sql_select(query)
                db_url_for_validation = resolve_db_url(self.hass, db_url)
                await self.hass.async_add_executor_job(
                    validate_query, db_url_for_validation, query, column
                )
            except NoSuchColumnError:
                errors["column"] = "column_invalid"
                description_placeholders = {"column": column}
            except MultipleResultsFound, MultipleQueryError:
                errors["query"] = "multiple_queries"
            except SQLAlchemyError:
                errors["db_url"] = "db_url_invalid"
            except NotSelectQueryError, UnknownQueryTypeError:
                errors["query"] = "query_no_read_only"
            except (TemplateError, EmptyQueryError, InvalidSqlQuery) as err:
                _LOGGER.debug("Invalid query: %s", err)
                errors["query"] = "query_invalid"
            else:
                recorder_db = get_instance(self.hass).db_url
                _LOGGER.debug(
                    "db_url: %s, resolved db_url: %s, recorder: %s",
                    db_url,
                    db_url_for_validation,
                    recorder_db,
                )

                mod_additional_options = {
                    k: v
                    for k, v in user_input[CONF_ADDITIONAL_OPTIONS].items()
                    if v is not None
                }
                user_input[CONF_ADDITIONAL_OPTIONS] = mod_additional_options

                return self.async_create_entry(
                    data=user_input,
                )

        return self.async_show_form(
            step_id="init",
            data_schema=self.add_suggested_values_to_schema(
                OPTIONS_SCHEMA, user_input or self.config_entry.options
            ),
            errors=errors,
            description_placeholders=description_placeholders,
        )
