import streamlit as st
import pandas as pd
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
import configparser
from loguru import logger
import socket
import time
import os
from io import BytesIO
import re
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import List, Dict, Union

# Настройка логирования
logger.add("script.log", rotation="10 MB")

# Загрузка конфигурационного файла с обработкой ошибок
def read_config() -> Dict[str, str]:
    config_file = 'config.ini'
    if not os.path.exists(config_file):
        logger.error(f"Файл конфигурации {config_file} не найден.")
        st.error(f"Файл конфигурации {config_file} не найден.")
        st.stop()
    
    config = configparser.ConfigParser()
    config.read(config_file)
    
    try:
        api_url = config['default']['API_URL']
        token = config['default']['YOUR_TOKEN']
        if not api_url:
            raise ValueError("API_URL не указан в файле конфигурации")
        if not token:
            raise ValueError("YOUR_TOKEN не указан в файле конфигурации")
        return {'API_URL': api_url, 'YOUR_TOKEN': token}
    except KeyError as e:
        logger.error(f"Ошибка конфигурации: {e}")
        st.error(f"Ошибка конфигурации: {e}")
        st.stop()
    except ValueError as ve:
        logger.error(str(ve))
        st.error(str(ve))
        st.stop()

# Загрузка конфигурации
config = read_config()
API_URL = config['API_URL'].rstrip('/')  # Убираем конечный слэш, если есть
YOUR_TOKEN = config['YOUR_TOKEN']

logger.info(f"API_URL: {API_URL}")
logger.info(f"YOUR_TOKEN: {'***' if YOUR_TOKEN else 'Не установлен'}")

# Функция для получения заголовков с токеном
def get_headers() -> Dict[str, str]:
    return {'Authorization': f'Token {YOUR_TOKEN}'}

# Функция для проверки интернет-соединения
def check_internet(host="8.8.8.8", port=53, timeout=3) -> bool:
    try:
        socket.setdefaulttimeout(timeout)
        socket.create_connection((host, port))
        logger.info("Интернет-соединение доступно.")
        return True
    except OSError as e:
        logger.error(f"Нет интернет-соединения: {e}")
        return False

# Функция для получения всех заемщиков
@st.cache_data(show_spinner=False)
def get_all_borrowers(limit: int = 250, delay: int = 5, max_retries: int = 3) -> List[Dict]:
    """
    Получает данные всех заемщиков через API.
    """
    all_data = []
    offset = 0
    retries = 0
    progress_bar = st.progress(0)
    status_text = st.empty()

    session = requests.Session()
    retry_strategy = Retry(
        total=max_retries,
        backoff_factor=1,
        status_forcelist=[500, 502, 503, 504],
        allowed_methods=["GET"]
    )
    adapter = HTTPAdapter(max_retries=retry_strategy)
    session.mount('https://', adapter)
    session.mount('http://', adapter)

    # Определение правильного endpoint для получения списка заемщиков
    # Возможно, вам нужно использовать другой путь. Проверьте документацию API.
    list_borrowers_endpoint = f"{API_URL}/borrowers"  # Возможно, нужен другой endpoint, например, /borrowers/list

    while True:
        try:
            url = f"{list_borrowers_endpoint}?limit={limit}&offset={offset}"
            logger.info(f"Запрос к URL: {url}")
            response = session.get(url, headers=get_headers(), timeout=30)
            logger.info(f"Ответ API: {response.status_code}")
            response.raise_for_status()

            data = response.json()
            logger.debug(f"Исходный ответ API: {data}")

            if isinstance(data, dict) and 'results' in data:
                data = data['results']
            elif isinstance(data, dict) and 'data' in data:
                data = data['data']
            elif isinstance(data, list):
                pass  # data уже является списком
            else:
                logger.error(f"Неожиданный формат данных: {type(data)}")
                break

            if not data:
                logger.info("Больше данных не получено.")
                break

            if isinstance(data, list):
                all_data.extend(data)
            elif isinstance(data, dict):
                all_data.append(data)
            else:
                logger.error(f"Неожиданный формат данных: {type(data)}")
                break

            logger.info(f"Получено {len(data)} записей.")
            status_text.text(f"Получено {len(all_data)} записей.")
            progress = len(all_data) / 1000  # Примерная оценка
            progress_bar.progress(min(progress, 1.0))

            if len(data) < limit:
                logger.info("Данные закончились.")
                break

            offset += limit
            time.sleep(delay)

        except requests.exceptions.HTTPError as http_err:
            if http_err.response.status_code == 404:
                logger.error(f"Ошибка 404 Not Found для URL {url}. Проверьте правильность endpoint.")
                st.error(f"Ошибка 404 Not Found для URL {url}. Проверьте правильность endpoint.")
                break  # Прекращаем попытки при 404
            else:
                logger.error(f"HTTP ошибка для URL {url}: {http_err}")
        except requests.exceptions.RequestException as e:
            logger.error(f"Ошибка запроса API: {e}")
            retries += 1
            if retries > max_retries:
                logger.error("Превышено максимальное количество попыток.")
                status_text.text("Превышено максимальное количество попыток.")
                break
            logger.info(f"Повтор после ошибки запроса. Попытка {retries}/{max_retries}")
            status_text.text(f"Повтор... Попытка {retries}/{max_retries}")
            time.sleep(delay * 2)

    logger.info(f"Всего получено записей: {len(all_data)}")
    status_text.text(f"Всего получено записей: {len(all_data)}")
    return all_data

# Функция для определения, является ли запись служебной
def is_service_data(entry: Dict) -> bool:
    """
    Определяет, является ли запись служебной.
    """
    if entry is None:
        return True
    if not isinstance(entry, dict):
        return True
    # Проверяем наличие служебных полей
    service_fields = {'type', 'level', 'title', 'checklist', 'short_title', 'account_group_role'}
    return service_fields.issubset(entry.keys())

# Функция для фильтрации заемщиков для отображения (исключая служебные записи)
def filter_borrowers_for_display(data: List[Dict]) -> List[Dict]:
    """
    Фильтрует данные, исключая служебные записи.
    """
    return [entry for entry in data if not is_service_data(entry)]

# Определите список служебных полей, которые нужно исключить из отображаемой таблицы
SERVICE_COLUMNS_TO_EXCLUDE = {
    'account_group_role',
    'another_service_field1',
    'another_service_field2'
    # Добавьте дополнительные поля по необходимости
}

# Функция для очистки и обработки значений для Excel
def clean_value(value: Union[str, int, float]) -> str:
    """Очищает и форматирует значение для Excel."""
    if isinstance(value, str):
        value = value.strip('"')
        value = re.sub(r'[\[\]{}\'"]', '', value)
        value = re.sub(r'\s+', ' ', value)
        return value.strip()
    return str(value)

# Функция для сохранения данных в XLSX с использованием pandas и xlsxwriter
def save_to_xlsx(data: List[Dict]) -> Union[bytes, None]:
    """
    Сохраняет данные в формат XLSX.
    """
    if not data:
        logger.error("Нет данных для сохранения в XLSX.")
        st.error("Нет данных для сохранения в XLSX.")
        return None
    
    output = BytesIO()
    try:
        # Используем pandas с движком xlsxwriter
        df = pd.DataFrame(data)
        
        # Запись всех данных, включая служебные
        with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
            df.to_excel(writer, index=False, sheet_name='Borrowers')
        
        xlsx_data = output.getvalue()
        output.close()
        return xlsx_data
    except Exception as e:
        logger.error(f"Ошибка сохранения в XLSX: {e}")
        st.error(f"Ошибка сохранения в XLSX: {e}")
        return None

# Функция для обработки загруженных Excel данных и извлечения ИНН
def process_excel_data(file) -> List[str]:
    """
    Обрабатывает Excel файл и извлекает ИНН.
    """
    try:
        # Чтение файла в DataFrame
        df = pd.read_excel(file)
        
        if df.empty:
            st.warning("Excel файл пуст.")
            return []
        
        # Определяем колонку с ИНН
        inn_column = next((col for col in df.columns if 'inn' in str(col).lower() or 'инн' in str(col).lower()), None)
        
        if inn_column is None:
            st.warning("Не удалось определить колонку с ИНН. Используется первая колонка.")
            inn_column = df.columns[0]
        
        # Извлекаем ИНН из выбранной колонки
        inn_data = df[inn_column].astype(str).apply(lambda x: re.findall(r'\b\d{10}\b|\b\d{12}\b', x))
        inn_data = inn_data[inn_data.apply(bool)].apply(lambda x: x[0])
        
        if inn_data.empty:
            st.error("Не удалось найти корректные данные ИНН.")
            return []
        
        inn_list = inn_data.dropna().tolist()
        logger.info(f"Извлечено {len(inn_list)} ИНН из Excel-файла.")
        st.success(f"Извлечено {len(inn_list)} ИНН из Excel-файла.")
        st.write(inn_list[:10] + ["..."] if len(inn_list) > 10 else inn_list)
        return inn_list
    except Exception as e:
        st.error(f"Ошибка обработки Excel-файла: {str(e)}")
        logger.error(f"Ошибка обработки Excel-файла: {str(e)}")
        return []

# Функция для отправки данных в API с улучшенной обработкой ошибок
def post_data(session: requests.Session, url: str, headers: Dict[str, str], inn: str) -> bool:
    """
    Отправляет данные ИНН в API.
    """
    data = {"inn": inn}
    try:
        response = session.post(url, headers=headers, json=data, timeout=10)
        response.raise_for_status()
        # Получение id из ответа, если необходимо
        response_data = response.json()
        request_id = response_data.get('id')
        if request_id:
            logger.info(f"Получен request_id {request_id} для ИНН {inn}")
        return True
    except requests.exceptions.HTTPError as http_err:
        if http_err.response.status_code == 405:
            logger.error(f"Ошибка 405 Method Not Allowed для ИНН {inn}. Проверьте настройки API.")
            st.error(f"Ошибка 405 Method Not Allowed для ИНН {inn}. Проверьте настройки API.")
        elif http_err.response.status_code == 404:
            logger.error(f"Ошибка 404 Not Found для ИНН {inn}. Проверьте правильность endpoint.")
            st.error(f"Ошибка 404 Not Found для ИНН {inn}. Проверьте правильность endpoint.")
        else:
            logger.error(f"HTTP ошибка при отправке ИНН {inn}: {http_err} - Response: {http_err.response.text}")
            st.error(f"HTTP ошибка при отправке ИНН {inn}: {http_err}.")
    except requests.exceptions.RequestException as req_err:
        logger.error(f"Ошибка запроса при отправке ИНН {inn}: {req_err}")
        st.error(f"Ошибка запроса при отправке ИНН {inn}: {req_err}.")
    return False

# Streamlit UI
def main():
    st.title("Панель администрирования обработки данных")

    # Sidebar для навигации
    page = st.sidebar.selectbox("Выберите задачу", ["Получить данные заёмщиков", "Обработать Excel-данные"])

    if page == "Получить данные заёмщиков":
        st.header("Получить данные заёмщиков")
        if st.button("Проверить интернет-соединение"):
            if check_internet():
                st.success("Интернет-соединение доступно.")
            else:
                st.error("Нет интернет-соединения.")

        if st.button("Получить данные заёмщиков"):
            with st.spinner("Получение данных..."):
                data = get_all_borrowers()
            if data:
                st.success(f"Получено {len(data)} записей.")
                
                # Сохранение всех данных в Streamlit session state
                st.session_state.borrowers_data = data
                st.session_state.enable_save = True

                # Фильтрация данных для отображения
                filtered_data = filter_borrowers_for_display(data)
                
                if filtered_data:
                    df_display = pd.DataFrame(filtered_data)
                    
                    # Исключение служебных полей из отображаемой таблицы
                    df_display = df_display.drop(columns=SERVICE_COLUMNS_TO_EXCLUDE, errors='ignore')
                    
                    st.dataframe(df_display)
                else:
                    st.info("Нет данных для отображения после фильтрации.")
            else:
                st.error("Нет данных, полученных от API.")
                # Отключение кнопки сохранения
                st.session_state.enable_save = False

        # Кнопка для сохранения данных в XLSX
        if st.button("Сохранить в XLSX", disabled=not st.session_state.get('enable_save', False)):
            if hasattr(st.session_state, 'borrowers_data'):
                xlsx_data = save_to_xlsx(st.session_state.borrowers_data)
                if xlsx_data:
                    st.download_button(
                        label="Скачать XLSX файл",
                        data=xlsx_data,
                        file_name="borrowers_data.xlsx",
                        mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
                    )
                else:
                    st.error("Ошибка при сохранении данных в XLSX.")
            else:
                st.error("Нет данных для сохранения. Пожалуйста, сначала получите данные.")

    elif page == "Обработать Excel-данные":
        st.header("Обработка Excel-данных")
        uploaded_file = st.file_uploader("Выберите Excel-файл", type="xlsx")
        if uploaded_file is not None:
            st.info("Файл загружен. Обработка...")
            inn_list = process_excel_data(uploaded_file)
            if inn_list:
                if st.button("Отправить данные в API"):
                    progress_bar = st.progress(0)
                    status_text = st.empty()
                    session = requests.Session()
                    retry_strategy = Retry(
                        total=5,
                        backoff_factor=0.1,
                        status_forcelist=[500, 502, 503, 504],
                        allowed_methods=["POST"]
                    )
                    adapter = HTTPAdapter(max_retries=retry_strategy)
                    session.mount('https://', adapter)
                    session.mount('http://', adapter)
                    headers = get_headers()
                    successful_count = 0
                    failed_count = 0

                    # Параллельная отправка запросов
                    with ThreadPoolExecutor(max_workers=10) as executor:
                        future_to_inn = {
                            executor.submit(post_data, session, f"{API_URL}/borrowers/new", headers, inn): inn
                            for inn in inn_list
                        }
                        total = len(future_to_inn)
                        for i, future in enumerate(as_completed(future_to_inn), 1):
                            inn = future_to_inn[future]
                            try:
                                if future.result():
                                    successful_count += 1
                                else:
                                    failed_count += 1
                            except Exception as exc:
                                logger.error(f"Исключение для ИНН {inn}: {exc}")
                                failed_count += 1

                            progress_bar.progress(i / total)
                            status_text.text(f"Обработано {i}/{total} ИНН")

                    st.success(f"Отправка данных завершена: {successful_count} успешных, {failed_count} неудачных")
            else:
                st.error("Не удалось извлечь ИНН из загруженного файла.")

if __name__ == "__main__":
    if 'enable_save' not in st.session_state:
        st.session_state.enable_save = False
    main() ошибка? 024-09-27 13:59:46.966 | INFO     | __main__:get_all_borrowers:100 - Запрос к URL: https://jetlend.ru/partner/api/borrowers?limit=250&offset=0
2024-09-27 13:59:47.621 | INFO     | __main__:get_all_borrowers:102 - Ответ API: 404
2024-09-27 13:59:47.623 | ERROR    | __main__:get_all_borrowers:144 - Ошибка 404 Not Found для URL https://jetlend.ru/partner/api/borrowers?limit=250&offset=0. Проверьте правильность endpoint.
2024-09-27 13:59:47.625 | INFO     | __main__:get_all_borrowers:160 - Всего получено записей: 0