"""
Сервис для импорта и экспорта товаров.

Этот модуль содержит логику импорта/экспорта товаров в различных форматах (CSV, Excel).
Разделение на отдельный модуль улучшает организацию кода и следует принципам SRP.
"""
import csv
import io
from decimal import Decimal, InvalidOperation
from django.utils import timezone
from ..models import Product

try:
    from openpyxl import load_workbook
except ImportError:
    load_workbook = None

import re


class ProductImporter:
    """
    Универсальный импорт товаров из CSV/XLSX.

    Поддерживаемые форматы:
    - CSV (UTF-8, заголовок в первой строке)
    - XLSX (первая строка — заголовки)

    Алгоритм:
    - Читаем файл → получаем headers и list[dict] строк
    - По headers строим маппинг на поля Product
    - Для каждой строки:
      - пропускаем полностью пустые строки
      - ищем товар по sku, потом по name
      - если update_existing=False и товар найден → пропуск
      - если update_existing=True → обновляем найденного
      - если не найден → создаём нового
    - Вся валидация делается через Product.full_clean()
    """

    FIELD_ALIASES = {
        "name": ["name", "название", "наименование", "товар", "продукт", "имя"],
        "sku": ["sku", "артикул", "код", "code", "article"],
        "price": ["price", "цена", "ценапродажи", "стоимость", "pricesale"],
        "description": ["description", "описание", "desc"],
        "short_description": ["shortdescription", "краткоеописание", "короткоеописание", "краткое"],
        "unit": ["unit", "единица", "ед", "едизм", "единицаизмерения"],
        "cost_price": ["costprice", "себестоимость", "закупочнаяцена", "cost"],
        "sale_price": ["saleprice", "ценасоскидкой", "скидка", "discount", "discountprice"],
        "images": ["images", "изображения", "фото", "картинки", "photos", "pictures", "image"],
    }

    def __init__(self):
        self.errors = []
        self.success_count = 0
        self.update_count = 0
        self.skip_count = 0
        # Сохраняем исходные данные для генерации error-файла
        self.original_headers = []
        self.original_rows = []
        self.file_format = None
        self.real_errors = []
        # Для Celery: собираем задачи загрузки фото
        self.photo_tasks = []

    def import_from_file(self, file, update_existing: bool = False, progress_callback=None, skip_images: bool = False, schema_name: str = None) -> dict:
        """
        Импорт товаров из загруженного файла.

        Args:
            file: UploadedFile
            update_existing: обновлять ли существующих товаров (по sku/name)
            progress_callback: callback для обновления прогресса (current, total, created, updated, skipped, errors)
            skip_images: пропустить загрузку фото (для Celery)
            schema_name: схема тенанта (для Celery задач фото)

        Returns:
            dict: результат импорта
        """
        file_format = self._detect_format(file)

        if file_format is None:
            return {
                "success": False,
                "message": "Неподдерживаемый формат файла. Ожидается CSV или XLSX.",
                "created": 0,
                "updated": 0,
                "skipped": 0,
                "errors": [{"row": None, "reason": "Unsupported file type"}],
            }

        if file_format == "xlsx" and load_workbook is None:
            return {
                "success": False,
                "message": "Для импорта XLSX необходим пакет openpyxl. Установите его и повторите попытку.",
                "created": 0,
                "updated": 0,
                "skipped": 0,
                "errors": [{"row": None, "reason": "openpyxl is not installed"}],
            }

        # Сохраняем формат файла
        self.file_format = file_format
        
        try:
            if file_format == "csv":
                headers, rows = self._read_csv(file)
            else:
                headers, rows = self._read_xlsx(file)
            
            # Сохраняем исходные данные
            self.original_headers = headers
            self.original_rows = rows
        except Exception as exc:
            return {
                "success": False,
                "message": f"Ошибка чтения файла: {exc}",
                "created": 0,
                "updated": 0,
                "skipped": 0,
                "errors": [{"row": None, "reason": str(exc)}],
            }

        if not headers:
            return {
                "success": False,
                "message": "В файле не найдены заголовки.",
                "created": 0,
                "updated": 0,
                "skipped": 0,
                "errors": [{"row": None, "reason": "Empty header row"}],
            }

        mapping = self._build_mapping(headers)

        # Минимальное требование: name ИЛИ sku
        if not any(field in mapping for field in ("name", "sku")):
            return {
                "success": False,
                "message": "Не удалось сопоставить обязательные поля (название или артикул).",
                "created": 0,
                "updated": 0,
                "skipped": len(rows),
                "errors": [
                    {
                        "row": None,
                        "reason": "No required fields (name/sku) mapped from headers",
                    }
                ],
            }

        for index, row in enumerate(rows, start=2):  # первая строка — заголовки
            self._process_row(index, row, mapping, update_existing, skip_images, schema_name)
            
            # Обновляем прогресс (если есть callback)
            if progress_callback:
                progress_callback(
                    current=index - 1,  # текущая строка
                    total=len(rows),
                    created=self.success_count,
                    updated=self.update_count,
                    skipped=self.skip_count,
                    errors=self.errors
                )

        total_errors = len(self.errors)
        real_error_count = len(self.real_errors)
        success = (self.success_count + self.update_count) > 0

        if success and total_errors == 0:
            message = "Импорт завершён успешно."
        elif success and total_errors > 0:
            message = "Импорт завершён с ошибками."
        else:
            message = "Не удалось импортировать данные."

        return {
            "success": success,
            "message": message,
            "created": self.success_count,
            "updated": self.update_count,
            "skipped": self.skip_count,
            "errors": self.errors,
            "real_errors": self.real_errors,
            "real_error_count": real_error_count,
            "photo_tasks": self.photo_tasks,  # Для Celery
        }

    def _detect_format(self, file) -> str | None:
        name = (getattr(file, "name", None) or "").lower()
        if name.endswith(".csv"):
            return "csv"
        if name.endswith(".xlsx") or name.endswith(".xls"):
            return "xlsx"
        return None

    def _read_csv(self, file):
        file.seek(0)
        raw = file.read()
        if isinstance(raw, bytes):
            text = raw.decode("utf-8-sig")
        else:
            text = raw
        f = io.StringIO(text)
        reader = csv.DictReader(f)
        headers = reader.fieldnames or []
        rows = list(reader)
        return headers, rows

    def _read_xlsx(self, file):
        file.seek(0)
        wb = load_workbook(file, read_only=True, data_only=True)
        ws = wb.active

        headers = []
        rows = []
        first_row = True

        for row in ws.iter_rows(values_only=True):
            if first_row:
                headers = [str(v).strip() if v is not None else "" for v in row]
                first_row = False
                continue

            if not any(row):
                continue

            row_dict = {}
            for idx, value in enumerate(row):
                if idx < len(headers):
                    header = headers[idx] or f"col_{idx}"
                    row_dict[header] = value
            rows.append(row_dict)

        return headers, rows

    def _normalize_header(self, header: str) -> str:
        if header is None:
            return ""
        cleaned = "".join(ch for ch in str(header).strip().lower() if ch.isalnum())
        return cleaned

    def _build_mapping(self, headers):
        mapping = {}
        normalized_aliases = {
            field: {self._normalize_header(a) for a in aliases}
            for field, aliases in self.FIELD_ALIASES.items()
        }

        for header in headers:
            norm = self._normalize_header(header)
            if not norm:
                continue
            for field, alias_set in normalized_aliases.items():
                if norm in alias_set and field not in mapping:
                    mapping[field] = header
                    break

        return mapping

    def _clean_value(self, value):
        if value is None:
            return ""
        return str(value).strip()

    def _parse_decimal(self, value: str) -> Decimal | None:
        """
        Парсинг decimal значения (цена, себестоимость).
        Обрабатывает разные форматы: запятая/точка, пробелы.
        """
        if not value:
            return None
        
        # Убираем пробелы
        value = value.replace(' ', '')
        
        # Заменяем запятую на точку
        value = value.replace(',', '.')
        
        try:
            return Decimal(value)
        except (InvalidOperation, ValueError):
            return None

    def _process_row(self, row_number: int, row: dict, mapping: dict, update_existing: bool, skip_images: bool = False, schema_name: str = None):
        name = self._clean_value(row.get(mapping.get("name", ""), ""))
        sku = self._clean_value(row.get(mapping.get("sku", ""), ""))
        price_raw = self._clean_value(row.get(mapping.get("price", ""), ""))
        description = self._clean_value(row.get(mapping.get("description", ""), ""))
        short_description = self._clean_value(row.get(mapping.get("short_description", ""), ""))
        unit = self._clean_value(row.get(mapping.get("unit", ""), ""))
        cost_price_raw = self._clean_value(row.get(mapping.get("cost_price", ""), ""))
        sale_price_raw = self._clean_value(row.get(mapping.get("sale_price", ""), ""))
        images_raw = self._clean_value(row.get(mapping.get("images", ""), ""))

        # Пропускаем полностью пустые строки
        if not any([name, sku, price_raw, description]):
            self.skip_count += 1
            return

        # Минимальное требование: name ИЛИ sku
        if not name and not sku:
            self.skip_count += 1
            error_record = {
                "row": row_number,
                "name": name or None,
                "sku": sku or None,
                "reason": "Требуется хотя бы одно: название или артикул",
            }
            self.errors.append(error_record)
            self.real_errors.append(error_record)
            return

        # Парсим цены
        price = self._parse_decimal(price_raw)
        cost_price = self._parse_decimal(cost_price_raw)
        sale_price = self._parse_decimal(sale_price_raw)

        # Цена обязательна
        if price is None:
            self.skip_count += 1
            error_record = {
                "row": row_number,
                "name": name or None,
                "sku": sku or None,
                "reason": "Требуется корректная цена (price)",
            }
            self.errors.append(error_record)
            self.real_errors.append(error_record)
            return

        # Единица измерения по умолчанию
        if not unit:
            unit = 'шт'
        
        # Находим единицу измерения в справочнике
        from products.models import UnitOfMeasure
        base_unit = UnitOfMeasure.objects.filter(code=unit, is_active=True).first()
        if not base_unit:
            # Пробуем найти по названию или короткому имени
            base_unit = UnitOfMeasure.objects.filter(
                Q(name__iexact=unit) | Q(short_name__iexact=unit),
                is_active=True
            ).first()
        if not base_unit:
            # Fallback на штуку
            base_unit = UnitOfMeasure.objects.filter(code='шт').first()
            if not base_unit:
                base_unit = UnitOfMeasure.objects.first()

        # Пытаемся найти существующего товара
        existing = None
        if sku:
            existing = Product.objects.filter(sku=sku, status='active').first()
        if existing is None and name:
            existing = Product.objects.filter(name=name, status='active').first()

        if existing and not update_existing:
            self.skip_count += 1
            self.errors.append({
                "row": row_number,
                "name": name or None,
                "sku": sku or None,
                "reason": "Товар с таким артикулом/названием уже существует, обновление отключено.",
            })
            return

        if existing and update_existing:
            # Обновление существующего товара
            if name:
                existing.name = name
            if sku:
                existing.sku = sku
            if description:
                existing.description = description
            if short_description:
                existing.short_description = short_description
            if unit:
                existing.base_unit = base_unit
            
            existing.price = price
            if cost_price is not None:
                existing.cost_price = cost_price
            if sale_price is not None:
                existing.sale_price = sale_price

            try:
                existing.full_clean()
                existing.save()
                self.update_count += 1
                
                # Обрабатываем изображения (если есть)
                if images_raw:
                    if skip_images:
                        # Для Celery: собираем задачи загрузки фото
                        self._collect_photo_tasks(existing, images_raw)
                    else:
                        # Синхронно загружаем фото
                        self._process_product_images(existing, images_raw)
                    
            except Exception as exc:
                self.skip_count += 1
                error_record = {
                    "row": row_number,
                    "name": name or None,
                    "sku": sku or None,
                    "reason": str(exc),
                }
                self.errors.append(error_record)
                self.real_errors.append(error_record)
            return

        # Создание нового товара
        product = Product(
            name=name or f"Товар {sku}",  # fallback если нет имени
            sku=sku or None,
            description=description or "",
            short_description=short_description or "",
            base_unit=base_unit,
            price=price,
            cost_price=cost_price or 0,  # Устанавливаем 0 вместо None (для CostPriceHistory)
            sale_price=sale_price,
            status='active',
        )

        try:
            product.full_clean()
            product.save()
            self.success_count += 1
            
            # Обрабатываем изображения (если есть)
            if images_raw:
                if skip_images:
                    # Для Celery: собираем задачи загрузки фото
                    self._collect_photo_tasks(product, images_raw)
                else:
                    # Синхронно загружаем фото
                    self._process_product_images(product, images_raw)
                
        except Exception as exc:
            self.skip_count += 1
            error_record = {
                "row": row_number,
                "name": name or None,
                "sku": sku or None,
                "reason": str(exc),
            }
            self.errors.append(error_record)
            self.real_errors.append(error_record)

    def _collect_photo_tasks(self, product, images_raw: str):
        """
        Собираем задачи загрузки фото для параллельной обработки через Celery.
        
        Args:
            product: Объект Product
            images_raw: Строка с URL изображений
        """
        # Разбиваем на отдельные URL
        urls = []
        for line in images_raw.split('\n'):
            line = line.strip()
            if not line:
                continue
            # Если в строке несколько URL через запятую
            for url in line.split(','):
                url = url.strip()
                if url.startswith('http'):
                    urls.append(url)
        
        if not urls:
            return
        
        # Проверяем есть ли уже главное фото
        has_main = product.photos.filter(is_main=True).exists()
        
        # Создаём задачи для каждого URL
        for idx, url in enumerate(urls):
            task = {
                'product_id': product.id,
                'url': url,
                'order': idx,
                'is_main': (idx == 0 and not has_main)  # Первое фото главное только если еще нет главного
            }
            self.photo_tasks.append(task)

    def _process_product_images(self, product, images_raw: str):
        """
        Обработка изображений товара из URL.
        
        Поддерживаемые форматы:
        - Один URL: https://example.com/image.jpg
        - Несколько URL через перенос строки или запятую
        
        Args:
            product: Объект Product
            images_raw: Строка с URL изображений
        """
        import requests
        from django.core.files.base import ContentFile
        from ..models import ProductPhoto
        import urllib.parse
        
        # Разбиваем на отдельные URL
        urls = []
        for line in images_raw.split('\n'):
            line = line.strip()
            if not line:
                continue
            # Если в строке несколько URL через запятую
            for url in line.split(','):
                url = url.strip()
                if url.startswith('http'):
                    urls.append(url)
        
        if not urls:
            return
        
        # Проверяем есть ли уже главное фото
        has_main = product.photos.filter(is_main=True).exists()
        
        # Скачиваем и сохраняем каждое изображение
        for idx, url in enumerate(urls):
            try:
                # Скачиваем изображение
                response = requests.get(url, timeout=10)
                response.raise_for_status()
                
                # Получаем имя файла из URL
                parsed_url = urllib.parse.urlparse(url)
                filename = parsed_url.path.split('/')[-1]
                
                # Создаём ProductPhoto
                photo = ProductPhoto(
                    product=product,
                    order=idx,
                    is_main=(idx == 0 and not has_main)  # Первое фото главное только если еще нет главного
                )
                
                # Сохраняем файл
                photo.image.save(
                    filename,
                    ContentFile(response.content),
                    save=True
                )
                
            except Exception as e:
                # Игнорируем ошибки загрузки изображений
                # чтобы не прерывать импорт товара
                continue

    def generate_error_file(self) -> tuple[bytes, str] | None:
        """
        Генерирует файл с ошибочными строками.
        
        Возвращает тот же формат, что был загружен (CSV или XLSX).
        Добавляет колонку "Ошибка" с описанием проблемы.
        
        Returns:
            tuple[bytes, str]: (file_content, filename) или None если нет ошибок
        """
        if not self.real_errors or not self.original_headers:
            return None
        
        # Создаём mapping row_number -> error
        error_map = {err['row']: err for err in self.real_errors if err.get('row')}
        
        if not error_map:
            return None
        
        # Собираем ошибочные строки
        error_rows = []
        for index, row in enumerate(self.original_rows, start=2):
            if index in error_map:
                error_info = error_map[index]
                row_with_error = dict(row)
                row_with_error['Ошибка'] = error_info['reason']
                error_rows.append(row_with_error)
        
        if not error_rows:
            return None
        
        headers_with_error = list(self.original_headers) + ['Ошибка']
        timestamp = timezone.now().strftime("%Y%m%d_%H%M%S")
        
        if self.file_format == 'csv':
            return self._generate_csv_error_file(headers_with_error, error_rows, timestamp)
        else:
            return self._generate_xlsx_error_file(headers_with_error, error_rows, timestamp)
    
    def _generate_csv_error_file(self, headers: list, rows: list[dict], timestamp: str) -> tuple[bytes, str]:
        """Генерирует CSV файл с ошибками."""
        output = io.StringIO()
        output.write('\ufeff')  # BOM для Excel
        
        writer = csv.DictWriter(output, fieldnames=headers, extrasaction='ignore')
        writer.writeheader()
        writer.writerows(rows)
        
        content = output.getvalue().encode('utf-8')
        filename = f'product_import_errors_{timestamp}.csv'
        
        return content, filename
    
    def _generate_xlsx_error_file(self, headers: list, rows: list[dict], timestamp: str) -> tuple[bytes, str] | None:
        """Генерирует XLSX файл с ошибками."""
        if load_workbook is None:
            return self._generate_csv_error_file(headers, rows, timestamp)
        
        try:
            from openpyxl import Workbook
            
            wb = Workbook()
            ws = wb.active
            ws.title = "Ошибки импорта"
            
            ws.append(headers)
            
            for row_dict in rows:
                row_data = [row_dict.get(h, '') for h in headers]
                ws.append(row_data)
            
            output = io.BytesIO()
            wb.save(output)
            output.seek(0)
            
            content = output.read()
            filename = f'product_import_errors_{timestamp}.xlsx'
            
            return content, filename
        except Exception:
            return self._generate_csv_error_file(headers, rows, timestamp)
