import json

from typing import Any, List


class QueryBuilder:
    def __init__(self, table_name: str, projection_attributes: List[str] = []):
        self._table_name = table_name
        self._projection_attributes = projection_attributes
        self._clauses = []

    @property
    def table_name(self):
        return self._table_name

    @property
    def projection_attributes(self) -> List[str]:
        return self._projection_attributes

    @property
    def clauses(self) -> List[str]:
        return self._clauses

    def add_string_clause(
        self,
        attribute: str,
        values: List[str]
    ) -> str:
        '''
        >>> qb = QueryBuilder('table_name')
        >>> qb.add_string_clause('t_environment', []) is None
        True

        >>> qb.add_string_clause('t_environment', ['PRD'])
        "t_environment = 'PRD'"

        >>> qb.add_string_clause('t_dcl', [1, 2])
        "t_dcl IN ('1','2')"

        >>> len(qb.clauses)
        2
        '''
        clause = None
        if attribute and values:
            if len(values) == 1:
                clause = f"{attribute} = '{values[0]}'"
            else:
                clause = f'{attribute} IN ('
                for val in sorted(values):
                    clause = f"{clause}'{val}',"
                clause = clause[:-1]  # remove hanging comma
                clause = f'{clause})'
            self._clauses.append(clause)
        return clause

    def add_numeric_clause(
        self,
        attribute: str,
        op: str,
        val: float,
        attribute_is_string: bool = False,
        places: int = None,
        precision: int = None
    ) -> str:
        '''
        >>> qb = QueryBuilder('table_name')
        >>> qb.add_numeric_clause('count', '=', None) is None
        True

        >>> qb.add_numeric_clause('count', '!=', 3.0)
        Traceback (most recent call last):
        AssertionError: Unrecognized numeric operator: !=

        >>> qb.add_numeric_clause('count', '=', 3.0)
        'count = 3.0'

        >>> qb.add_numeric_clause('count', '<', 4.0, attribute_is_string=True)
        'TRY_CAST(count AS DECIMAL) < 4.0'

        >>> qb.add_numeric_clause('count', '>', 1.0, attribute_is_string=True, places=4, precision=2)
        'TRY_CAST(count AS DECIMAL(4, 2)) > 1.0'

        >>> len(qb.clauses)
        3
        '''
        clause = None
        assert op in ['=', '<>', '<', '>', '<=', '>='], f'Unrecognized numeric operator: {op}'
        if attribute and op and val:
            if attribute_is_string and places and precision:
                clause = f'TRY_CAST({attribute} AS DECIMAL({places}, {precision})) {op} {val}'
            elif attribute_is_string:
                clause = f'TRY_CAST({attribute} AS DECIMAL) {op} {val}'
            else:
                clause = f'{attribute} {op} {val}'
            self._clauses.append(clause)
        return clause

    def add_numeric_list_clause(
        self,
        attribute: str,
        values: List[Any],
        attribute_is_string: bool = False,
        places: int = None,
        precision: int = None
    ) -> str:
        '''
        >>> qb = QueryBuilder('table_name')
        >>> qb.add_numeric_list_clause('count', []) is None
        True

        >>> qb.add_numeric_list_clause('count', [1])
        'count = 1'

        >>> qb.add_numeric_list_clause('recount', [2, 3])
        'recount IN (2,3)'

        >>> qb.add_numeric_list_clause('count', [4], attribute_is_string=True)
        'TRY_CAST(count AS DECIMAL) = 4'

        >>> qb.add_numeric_list_clause('count', [5], attribute_is_string=True, places=5, precision=3)
        'TRY_CAST(count AS DECIMAL(5, 3)) = 5'

        >>> len(qb.clauses)
        4
        '''
        clause = None
        if attribute and values:
            if attribute_is_string and places and precision:
                clause = f'TRY_CAST({attribute} AS DECIMAL({places}, {precision}))'
            elif attribute_is_string:
                clause = f'TRY_CAST({attribute} AS DECIMAL)'
            else:
                clause = f'{attribute}'

            if len(values) == 1:
                clause = f"{clause} = {values[0]}"
            else:
                clause = f'{clause} IN ('
                for val in sorted(values):
                    clause = f"{clause}{val},"
                clause = clause[:-1]  # remove hanging comma
                clause = f'{clause})'
            self._clauses.append(clause)
        return clause

    def add_date_clause(
        self,
        attribute: str,
        op: str,
        val: str,
        attribute_is_string: bool = False
    ) -> str:
        '''
        >>> qb = QueryBuilder('table_name')
        >>> qb.add_date_clause('report_date', '=', None) is None
        True

        >>> qb.add_date_clause('report_date', '!=', '2021-04-25')
        Traceback (most recent call last):
        AssertionError: Unrecognized date operator: !=

        >>> qb.add_date_clause('report_date', '>', '2021-04-25')
        "report_date > DATE('2021-04-25')"

        >>> qb.add_date_clause('found_date', '<', '2021-06-15', attribute_is_string=True)
        "TRY_CAST(SUBSTRING(found_date, 1, 10) AS DATE) < DATE('2021-06-15')"

        >>> len(qb.clauses)
        2
        '''
        clause = None
        assert op in ['=', '<>', '<', '>', '<=', '>='], f'Unrecognized date operator: {op}'
        if attribute and op and val:
            if attribute_is_string:
                clause = f"TRY_CAST(SUBSTRING({attribute}, 1, 10) AS DATE) {op} DATE('{val}')"
            else:
                clause = f"{attribute} {op} DATE('{val}')"
            self._clauses.append(clause)
        return clause

    def render_sql(self) -> str:
        '''
        >>> qb = QueryBuilder('table_name', ['account', 'region'])
        >>> qb.render_sql()
        'SELECT account, region FROM table_name;'

        >>> qb = QueryBuilder('table_name')
        >>> qb.render_sql()
        'SELECT * FROM table_name;'

        >>> _ = qb.add_string_clause('user', ['nelson.mandela@sa.com'])
        >>> qb.render_sql()
        "SELECT * FROM table_name WHERE user = 'nelson.mandela@sa.com';"

        >>> _ = qb.add_numeric_clause('count', '=', 10, attribute_is_string=True)
        >>> qb.render_sql()
        "SELECT * FROM table_name WHERE TRY_CAST(count AS DECIMAL) = 10 AND user = 'nelson.mandela@sa.com';"
        '''
        projection_attributes = '*'
        if self._projection_attributes:
            sep = ', '
            projection_attributes = sep.join(self._projection_attributes)

        sql = f'SELECT {projection_attributes} FROM {self._table_name}'
        if self._clauses:
            sql += ' WHERE '
            sep = ' AND '
            sql += sep.join(sorted(self._clauses))
        return f'{sql};'

    def __repr__(self) -> str:
        return json.dumps({
            'table_name': self._table_name,
            'projection_attributes': self._projection_attributes,
            'clauses': self._clauses
        })


if __name__ == '__main__':
    import doctest
    doctest.testmod()
