import requests
from datetime import datetime,timedelta
import clickhouse_connect
import pandas as pd
import os
from dateutil import parser
import time
import hashlib
from io import StringIO
import chardet
import json
import math
from transliterate import translit


# --- Глобальный реестр прокси ---------------------------------------------
# Намеренное расхождение с Morin (см. AGENTS.md §7.1).
# upload_main.py читает лист `Proxy` из settings.xlsx и один раз вызывает
# set_global_proxies(...). Все экземпляры Common() без явного аргумента
# подхватывают этот словарь автоматически. Если лист отсутствует / пуст —
# словарь остаётся пустым, send_logs идёт прямым запросом (как в 1.0.9).
_GLOBAL_PROXIES: dict = {}

def set_global_proxies(proxies: dict):
    """Вызывается один раз из upload_main.py на старте контейнера."""
    global _GLOBAL_PROXIES
    _GLOBAL_PROXIES = dict(proxies or {})


class Common:
    def __init__(self, bot_token:str = '', chat_list:str = '', message_type: str = '', proxies: dict = None):
        self.bot_token = bot_token
        self.chat_list = chat_list
        # Реестр прокси по имени-тегу (лист Proxy из settings.xlsx).
        # Формат: {'telegram': {'url': 'http://user:pass@host:port',
        #                       'fallback_direct': True, 'timeout_sec': 15}, ...}
        # Намеренное расхождение с Morin (см. AGENTS.md §7.1).
        # Если явный proxies не передан — берём из глобала (set_global_proxies).
        self.proxies = proxies if proxies is not None else _GLOBAL_PROXIES

        if message_type == 'all':
            self.value = 1
        elif message_type == 'key':
            self.value = 2
        else:
            self.value = 3
        self.now = datetime.now()
        self.today = datetime.now().date()

    def running_in_airflow(self):
        return any(k.startswith("AIRFLOW_") for k in os.environ.keys())

    def log_func(self, bot_token, chat_ids,message, value):
        try:
            print(message)
            log_file_path = "/app/log.txt"
            if not self.running_in_airflow():
                log_dir = "/app/logs"
                os.makedirs(log_dir, exist_ok=True)
                log_file_path = os.path.join(log_dir, "log.txt")
            else:
                os.makedirs(os.path.dirname(log_file_path), exist_ok=True)
            if value >= self.value:
                with open(log_file_path, "a", encoding="utf-8") as log_file:
                    log_file.write(message + "\n\n")
            self.send_logs_clear(bot_token, chat_ids, message)
        except Exception as e:
            print(f'Ошибка log_func: {e}')

    def send_logs_clear(self,bot_token, chat_ids, message):
        try:
            log_file_path = "/app/log.txt"
            if not self.running_in_airflow():
                log_file_path = "/app/logs/log.txt"
            if not os.path.exists(log_file_path):
                if not self.running_in_airflow():
                    os.makedirs(os.path.dirname(log_file_path), exist_ok=True)
                else:
                    print("Файл лога не существует.")
            with open(log_file_path, "r") as log_file:
                content = log_file.read()
            if len(content) > 1000:
                self.message_text = content
                self.send_logs(bot_token, chat_ids)
                with open(log_file_path, "w") as log_file:
                    log_file.write("")  # Очищаем файл
                print("Файл очищен, длина содержимого превышала 1000 символов.")
            return content
        except Exception as e:
            print(f'Ошибка send_logs_clear: {e}')

    def send_logs_clear_anyway(self,bot_token, chat_ids):
        try:
            log_file_path = "/app/log.txt"
            if not self.running_in_airflow():
                log_file_path = "/app/logs/log.txt"
            if not os.path.exists(log_file_path):
                print("Файл лога не существует.")
            with open(log_file_path, "r") as log_file:
                content = log_file.read()
            if len(content.strip()) > 0:
                self.message_text = content.strip()
                self.send_logs(bot_token, chat_ids)
                with open(log_file_path, "w") as log_file:
                    log_file.write("")  # Очищаем файл
                print("Файл очищен, длина содержимого превышала 1000 символов.")
            return content
        except Exception as e:
            print(f'Ошибка send_logs_clear: {e}')

    def send_logs(self, bot_token, chat_ids):
        # Намеренное расхождение с Morin (см. AGENTS.md §7.1):
        # прокси берём из self.proxies['telegram'] (лист Proxy settings.xlsx),
        # а не из захардкоженного URL в коде. Если прокси не настроен — прямой запрос.
        if not bot_token or not chat_ids:
            return
        try:
            url = f'https://api.telegram.org/bot{bot_token}/sendMessage'
            tg_proxy = self.proxies.get('telegram') or {}
            proxy_url = tg_proxy.get('url') or ''
            fallback_direct = tg_proxy.get('fallback_direct', True)
            timeout = tg_proxy.get('timeout_sec') or 15
            for chat_id in chat_ids:
                params = {'chat_id': chat_id, 'text': self.message_text}
                response = None
                # 1) Пробуем через прокси, если сконфигурирован
                if proxy_url:
                    try:
                        response = requests.get(
                            url, params=params,
                            proxies={'http': proxy_url, 'https': proxy_url},
                            timeout=timeout,
                        )
                    except Exception as e:
                        print(f"Прокси недоступен для чата {chat_id}: {e}")
                        response = None
                # 2) Fallback — прямой запрос (если прокси не задан, либо упал и разрешён fallback)
                if response is None and (not proxy_url or fallback_direct):
                    try:
                        response = requests.get(url, params=params, timeout=timeout)
                    except Exception as e:
                        print(f"Ошибка отправки сообщения в чат {chat_id}: {e}")
                        continue
                if response is None:
                    print(f"Ошибка отправки сообщения в чат {chat_id}: прокси упал, fallback отключён")
                    continue
                if response.status_code != 200:
                    print(f"Ошибка отправки сообщения в чат {chat_id}: {response.text}")
            self.message_text = ''
        except Exception as e:
            print(f'Ошибка send_logs: {e}')

    def is_empty(self, result):
        if not result:
            return True
        if isinstance(result, list) and all(isinstance(item, dict) and not item for item in result):
            return True
        return False

    def flip_date(self, date_text):
        try:
            year, month, day = date_text.split('-')
            return f"{day}.{month}.{year}"
        except Exception as e:
            print(f'Ошибка flip_date: {e}')

    def datetime_to_unixtime(self, datetime_str):
        # Парсим строку даты и времени в объект datetime
        date_obj = datetime.strptime(datetime_str, '%Y-%m-%d %H:%M:%S')
        # Преобразуем в Unix timestamp (в секундах)
        timestamp = int(date_obj.timestamp())
        return timestamp

    def get_month_start(self, date_str: str):
        try:
            date_obj = datetime.strptime(date_str, '%Y-%m-%d')
            month_start = date_obj.replace(day=1)
            return month_start.strftime('%Y-%m-%d')
        except Exception as e:
            print(f'Ошибка get_month_start: {e}')

    def is_error(self, result):
        if isinstance(result, str):
            if 'Ошибка:' in result:
                return True
        return False

    def shorten_text(self, text):
        # Используем хеш-функцию md5 для сокращения строки
        hash_object = hashlib.md5(text.encode())  # Можно также использовать sha256
        return hash_object.hexdigest()[:10]  # Возвращаем первые 10 символов хеша

    def transliterate_key(self, key):
        import re
        key = re.sub(r'%!\(EXTRA[^)]*\)', '', key)
        tr = translit(key, 'ru', reversed=True)
        tr = tr.strip().replace('%','').replace('₽','').replace('№','').replace('"','').replace('«','').replace('»','').replace('!','').replace('=','').replace(':','_').replace(' ', '_').replace('-', '_').replace(",", '').replace("'", '').replace(".", '').replace("(",'').replace(")", '').lower().strip().strip('_')
        return tr

    def transliterate_dict_keys_in_list(self, dictionaries_list):
        updated_list = []
        for dictionary in dictionaries_list:
            updated_dict = {}
            for key, value in dictionary.items():
                new_key = self.transliterate_key(key)
                updated_dict[new_key] = value
            updated_list.append(updated_dict)
        return updated_list


    def tuple_none_change(self,my_tuple):
        my_list = list(my_tuple)
        counter = 1
        for i in range(len(my_list)):
            if my_list[i] is None:
                my_list[i] = f'None{counter}'
                counter += 1
            else:
                my_list[i] = my_list[i].replace('№','nomer').replace('%','percent').replace('/','slash').replace('\\','slash').replace('(','').replace(')','')
        new_tuple = tuple(my_list)
        return new_tuple


    def replace_keys_in_data(self, dictionaries_list):
        updated_list = []
        for dictionary in dictionaries_list:
            updated_dict = {}
            for key, value in dictionary.items():
                # Заменяем дефисы на подчеркивания в ключе
                new_key = key.replace('-', '_')
                # Добавляем новый ключ и значение в обновленный словарь
                updated_dict[new_key] = value
            updated_list.append(updated_dict)
        return updated_list

    def shift_date(self, date_str, days=7):
        # Преобразуем строку в объект datetime
        date_obj = datetime.strptime(date_str, '%Y-%m-%d')
        # Сдвигаем дату на указанное количество дней назад
        new_date = date_obj - timedelta(days=days)
        # Преобразуем дату обратно в строку
        return new_date.strftime('%Y-%m-%d')


    def keep_last_20000_lines(self,file_path):
        with open(file_path, 'rb') as file:
            raw_data = file.read()
            result = chardet.detect(raw_data)
            encoding = result['encoding']
        with open(file_path, 'r', encoding=encoding) as file:
            lines = file.readlines()
        last_20000_lines = lines[-20000:]
        with open(file_path, 'w', encoding=encoding) as file:
            file.writelines(last_20000_lines)

    # значение -> тип значения для clickhouse
    def get_data_type(self, column, getvalue, partitions):
        getvalue = str(getvalue)
        part_list = partitions.replace(' ', '').split(',')
        if getvalue == None or getvalue == 'None' or getvalue.strip() == '': return 'None'
        if getvalue.lower() == 'false' or getvalue.lower() == 'true':
            return 'UInt8'
        date_formats = [
            "%Y-%m-%dT%H:%M:%S.%f%z",  # 2023-10-22T16:36:15.507+0000
            "%Y-%m-%d %H:%M:%S.%f%z",  # 2023-10-22 16:36:15.507+0000
            "%Y-%m-%dT%H:%M:%S%z",  # 2023-10-22T16:36:15+0000
            "%Y-%m-%d %H:%M:%S%z",  # 2023-10-22 16:36:15+0000
            "%Y-%m-%dT%H:%M:%S.%f",  # 2023-10-22T16:36:15.507 (без таймзоны)
            "%Y-%m-%d %H:%M:%S.%f",  # 2023-10-22 16:36:15.507 (без 'T')
            "%Y-%m-%dT%H:%M:%S",  # 2023-10-22T16:36:15 (без миллисекунд и таймзоны)
            "%Y-%m-%d %H:%M:%S",  # 2023-10-22 16:36:15 (без 'T', без миллисекунд)
            "%Y-%m-%d",  # 2023-10-22 (только дата)
            "%Y.%m.%d",  # 2023-10-22 (только дата)
            "%d.%m.%Y",  # 22-10-2023 (европейский формат)  # Формат Date с днем в начале: 08-09-2021
            "%d-%m-%Y",  # 22-10-2023 (европейский формат)  # Формат Date с днем в начале: 08-09-2021
            '%Y/%m/%d',  # Формат Date через слэш: 2024/09/01
            '%H:%M:%S',  # Формат Time: 21:20:10
        ]
        for date_format in date_formats:
            try:
                parsed_date = datetime.strptime(getvalue.replace('Z', ''), date_format)
                # Если дата меньше 1970 года — это не допустимая дата для ClickHouse
                if parsed_date.year < 1970:
                    return 'String'
                # Определяем тип на основе формата
                if date_format in ['%Y-%m-%d', '%d-%m-%Y','%Y.%m.%d', '%d.%m.%Y', '%Y/%m/%d']:
                    return 'Date'  # Это формат Date
                elif date_format == '%H:%M:%S':
                    return 'Time'  # Это формат Time
                else:
                    return 'DateTime'  # Форматы с датой и временем
            except ValueError:
                continue
        try:
            float_value = float(getvalue)
            if len(str(float_value)) < 15 and column not in part_list:
                return 'Float64'
        except:
            pass
        return 'String'


    def column_to_datetime(self, date_str):
        if pd.isna(date_str):
            return None
        date_str = date_str.strip()

        # Обрабатываем таймзону 'Z' (UTC) и заменяем на '+0000'
        if date_str.endswith('Z'):
            date_str = date_str[:-1] + '+0000'
        # Обрабатываем таймзоны вида +00:00 и заменяем на +0000
        elif '+' in date_str and date_str.endswith(':00'):
            date_str = date_str[:-3] + date_str[-2:]

        date_formats = [
            "%Y-%m-%dT%H:%M:%S.%f%z",  # 2023-10-22T16:36:15.507+0000
            "%Y-%m-%d %H:%M:%S.%f%z",  # 2023-10-22 16:36:15.507+0000
            "%Y-%m-%dT%H:%M:%S%z",  # 2023-10-22T16:36:15+0000
            "%Y-%m-%d %H:%M:%S%z",  # 2023-10-22 16:36:15+0000
            "%Y-%m-%dT%H:%M:%S.%f",  # 2023-10-22T16:36:15.507 (без таймзоны)
            "%Y-%m-%d %H:%M:%S.%f",  # 2023-10-22 16:36:15.507 (без 'T')
            "%Y-%m-%dT%H:%M:%S",  # 2023-10-22T16:36:15 (без миллисекунд и таймзоны)
            "%Y-%m-%d %H:%M:%S",  # 2023-10-22 16:36:15 (без 'T', без миллисекунд)
            "%Y-%m-%d",  # 2023-10-22 (только дата)
            "%d-%m-%Y",  # 22-10-2023 (европейский формат)
            "%Y.%m.%d",  # 2023-10-22 (только дата)
            "%d.%m.%Y"  # 22-10-2023 (европейский формат)
        ]

        for fmt in date_formats:
            try:
                dt = datetime.strptime(date_str, fmt)
                return dt.strftime("%Y-%m-%d %H:%M:%S.%f")
            except ValueError:
                continue
        return None
        # список словарей (данные) -> список поле_типданных

    def analyze_column_types(self, data, uniq_columns, partitions, text_columns_set):
        try:

            null_columns = []
            column_types = {}
            # Проходим по всем строкам в данных
            for row in data:
                for column, anvalue in row.items():
                    value_type = self.get_data_type(column, anvalue, partitions)  # Определяем тип данных
                    if column not in column_types:
                        column_types[column] = set()  # Создаем множество для уникальных типов
                    column_types[column].add(value_type)
            # Приводим типы столбцов к общему типу
            final_column_types = {}
            for column, types in column_types.items():
                try: types.remove('None')
                except: pass
                if len(types) == 1 and column.strip() not in  text_columns_set:
                    final_column_types[column] = next(iter(types))
                elif len(types) == 0:
                    final_column_types[column] = 'None'
                else:
                    final_column_types[column] = 'String'  # Если разные типы, делаем строкой
            create_table_query = []
            # non_nullable_list = uniq_columns.replace(' ','').split(',')+[partitions.strip()]
            for field, data_type in final_column_types.items():
                create_table_query.append(f"{field} {data_type}")
        except Exception as e:
            message = f'Функция: analyze_column_types. Ошибка: {e}'
            self.log_func(self.bot_token, self.chat_list,message, 3)


        return create_table_query

    # список словарей (данные) -> датафрейм с нужными типами
    def check_and_convert_types(self, data, uniq_columns, partitions, text_columns_set):
        try:
            columns_list=self.analyze_column_types(data, uniq_columns, partitions,text_columns_set)
            df=pd.DataFrame(data,dtype=str)
            type_mapping = {
                'UInt8': 'bool',
                'Date': 'datetime64[ns]',  # pandas формат для дат
                'DateTime': 'datetime64[ns]',  # pandas формат для дат с временем
                'String': 'object',  # Строковый формат в pandas
                'Float64': 'float64',  # float64 тип в pandas
            }
            for item in columns_list:
                column_name, expected_type = item.split()  # Разделяем по пробелу: 'column_name expected_type'
                if column_name in df.columns:
                    expected_type = expected_type.strip()
                    try:
                        if expected_type in ['Date']:
                            df[column_name] = df[column_name].apply(self.column_to_datetime)
                            df[column_name] = pd.to_datetime(df[column_name], errors='raise').dt.date
                            df[column_name] = df[column_name].fillna(pd.to_datetime('1970-01-01').date())
                        if expected_type in ['DateTime']:
                            df[column_name] = df[column_name].apply(self.column_to_datetime)
                            df[column_name] = pd.to_datetime(df[column_name], errors='raise')
                            df[column_name] = df[column_name].fillna(pd.Timestamp('1970-01-01'))
                        elif expected_type in ['UInt8']:
                            df[column_name] = df[column_name].replace({'True': True, 'False': False, 'true': True, 'false': False, })
                            df[column_name] = df[column_name].fillna(False)
                            df[column_name] = df[column_name].astype('bool')
                        elif expected_type in ['Float64']:
                            df[column_name] = pd.to_numeric(df[column_name], errors='raise').astype('float64')
                            df[column_name] = df[column_name].fillna(0)
                        elif expected_type in ['String']:
                            df[column_name] = df[column_name].astype(str)
                            df[column_name] = df[column_name].fillna("")
                        elif 'None' in expected_type:
                            df = df.drop(columns=[column_name])
                    except Exception as e:
                        message = f"Функция: check_and_convert_types. Ошибка при преобразовании столбца '{column_name}': {e}"
                        self.log_func(self.bot_token, self.chat_list, message, 3)
            df['timeStamp'] = self.now
            message = f'Функция: check_and_convert_types. Датафрейм успешно преобразован'
            self.log_func(self.bot_token, self.chat_list, message, 2)
        except Exception as e:
            message= f'Функция: check_and_convert_types. Ошибка преобразования df: {e}'
            self.log_func(self.bot_token, self.chat_list, message, 3)
        return df

    def to_collect(self, schedule_str, date_str):
        try:
            today = datetime.strptime(date_str, '%Y-%m-%d').date()
        except ValueError:
            raise ValueError("Дата должна быть в формате 'YYYY-MM-DD'")
        day_of_week = today.strftime('%A').lower()  # День недели (например, 'friday')
        day_of_month = today.day  # Число месяца (например, 22)
        schedule_list = [s.strip().lower() for s in schedule_str.split(',')]
        for schedule in schedule_list:
            if schedule == 'daily':  # Если указано "daily", всегда возвращаем True
                return True
            if schedule == day_of_week:  # Проверка дня недели (например, 'friday')
                return True
            if schedule.isdigit() and int(schedule) == day_of_month:  # Проверка числа месяца
                return True
        return False

    # _camel_to_snake / _flatten_dict_deep удалены в v1.1.0:
    # это был антипаттерн из BUG-002, нигде не использовался; правильный способ — spread_table ниже.

    def spread_table(self, source_list):
        result_list = []
        for row in source_list:
            row_dict = {}
            for key, value in row.items():
                if isinstance(value, dict):
                    for name, inner_value in dict(value).items():
                        row_dict[f'{key}_{name}'] = inner_value
                else:
                    row_dict[f'{key}'] = value
            result_list.append(row_dict)
        return result_list

    def get_chunks(self, lst, n):
        clean_lst = [int(x) for x in lst]
        result = []
        for i in range(0, len(clean_lst), n):
            result.append(clean_lst[i: i + n])
        return result
