import base64
import json
from datetime import date, datetime
from typing import Any, Iterable
from urllib import error, parse, request


class ClickHouseClient:
    def __init__(self, config: dict[str, Any]):
        self.scheme = str(config.get('scheme', 'http') or 'http')
        self.host = str(config.get('host', '') or '').strip()
        self.port = int(config.get('port', 8123) or 8123)
        self.database = str(config.get('database', 'clickhouse') or 'clickhouse')
        self.username = str(config.get('username', 'default') or 'default')
        self.password = str(config.get('password', '') or '')
        self.timeout_seconds = max(int(config.get('timeout_seconds', 30) or 30), 2)

        if self.host == '':
            raise RuntimeError('ClickHouse host is not configured.')

    def qualified_table(self, table: str) -> str:
        return f"{self.quote_identifier(self.database)}.{self.quote_identifier(table)}"

    def quote_identifier(self, identifier: str) -> str:
        identifier = str(identifier or '').strip()
        if identifier == '' or any(ch not in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_' for ch in identifier):
            raise RuntimeError(f'Invalid ClickHouse identifier: {identifier}')
        return f'`{identifier}`'

    def escape(self, value: Any) -> str:
        return str(value).replace('\\', '\\\\').replace('\n', '\\n').replace('\r', '\\r').replace('\t', '\\t').replace("'", "\\'")

    def command(self, sql: str) -> str:
        return str(self._request(sql.strip(), expect_json=False))

    def select(self, sql: str) -> list[dict[str, Any]]:
        payload = self._request(sql.strip() + "\nFORMAT JSON", expect_json=True)
        if not isinstance(payload, dict):
            raise RuntimeError('Invalid ClickHouse SELECT response payload.')
        data = payload.get('data')
        if not isinstance(data, list):
            raise RuntimeError('Invalid ClickHouse SELECT response data.')
        return data

    def insert_json_rows(self, table: str, rows: Iterable[dict[str, Any]], columns: list[str] | None = None) -> int:
        rows = list(rows)
        if not rows:
            return 0

        if columns is None:
            columns = list(rows[0].keys())

        lines = []
        for row in rows:
            ordered = {column: self._normalize_insert_value(row.get(column)) for column in columns if column in row}
            lines.append(json.dumps(ordered, ensure_ascii=False, separators=(',', ':')))

        column_sql = ', '.join(self.quote_identifier(column) for column in columns)
        query = f"INSERT INTO {self.qualified_table(table)} ({column_sql}) FORMAT JSONEachRow\n" + "\n".join(lines)
        self.command(query)
        return len(rows)

    def _normalize_insert_value(self, value: Any) -> Any:
        if isinstance(value, datetime):
            return value.strftime('%Y-%m-%d %H:%M:%S')
        if isinstance(value, date):
            return value.strftime('%Y-%m-%d')
        if isinstance(value, bool):
            return 1 if value else 0
        return value

    def _request(self, query: str, expect_json: bool) -> dict[str, Any] | str:
        url = f"{self.scheme}://{self.host}:{self.port}/?database={parse.quote(self.database)}"
        payload = query.encode('utf-8')
        req = request.Request(url, data=payload, method='POST')
        req.add_header('Content-Type', 'text/plain')
        if expect_json:
            req.add_header('Accept', 'application/json')

        token = base64.b64encode(f"{self.username}:{self.password}".encode('utf-8')).decode('ascii')
        req.add_header('Authorization', f'Basic {token}')

        try:
            with request.urlopen(req, timeout=self.timeout_seconds) as response:
                body = response.read().decode('utf-8')
        except error.HTTPError as exc:
            body = exc.read().decode('utf-8', errors='replace')
            raise RuntimeError(
                f"ClickHouse query failed with status {exc.code}; body: {body[:500]}"
            ) from exc
        except Exception as exc:
            raise RuntimeError(f"ClickHouse request failed: {exc}") from exc

        if expect_json:
            try:
                return json.loads(body)
            except json.JSONDecodeError as exc:
                raise RuntimeError('ClickHouse JSON response was invalid.') from exc

        return body
