"""
Универсальный читатель "легаси" .doc.

Стратегия (управляется `DocReadMethod`, аналог `$Script:DocReadMethod` в ps1,
но переработана в понятный каскад без ручного управления очередями retry):

    AUTO:
        1) если реальный формат оказался ZIP/RTF/HTML/XML — сразу используем
           специализированный парсер (самый надёжный вариант);
        2) для настоящего OLE2-бинарника пробуем, в порядке убывания точности:
              a. LibreOffice headless (если найден soffice/soffice.exe) —
                 конвертирует .doc в .txt почти со 100% точностью, работает
                 на Windows/Linux/macOS;
              b. MS Word через COM (только Windows + установленный Word);
              c. Эвристический побайтовый разбор (гарантированно доступен
                 везде, без внешних зависимостей, но менее точен).
    LIBREOFFICE / COM / HEURISTIC — принудительно только указанный метод.

Критическое исправление v4 (метод Карпати):
    * Устранена утечка COM-объектов Word в многопоточном режиме: реестр
      _word_registry отслеживает все потоки, создавшие Word.Application,
      а _release_word_all() освобождает их гарантированно после завершения
      пула потоков (вызывается из scanner.py).
    * _via_com не кэширует Word между разными вызовами extract_text в
      пределах одного потока — каждый вызов создаёт и закрывает экземпляр,
      что исключает утечки при досрочной отмене сканирования.
"""
from __future__ import annotations

import gc
import platform
import subprocess
import tempfile
from pathlib import Path
from typing import Optional

from app.config import DocReadMethod
from app.readers import doc_formats
from app.readers.base import BaseReader, ReaderOutcome
from app.settings_store import load_settings


# --------------------------------------------------------------------------- #
# LibreOffice
# --------------------------------------------------------------------------- #

def _find_libreoffice() -> Optional[str]:
    s = load_settings()
    if s.libreoffice_path and Path(s.libreoffice_path).exists():
        return s.libreoffice_path
    return None


# --------------------------------------------------------------------------- #
# MS Word COM — гарантированное освобождение через реестр всех экземпляров.
# Каждый вызов _via_com создаёт Word.Application и сразу закрывает его
# (без кэширования между файлами), что исключает утечки при отмене скана.
# --------------------------------------------------------------------------- #


def _release_word_all() -> None:
    """Освобождает ВСЕ экземпляры Word.Application, созданные через _via_com
    в любых потоках. Вызывается однократно из scanner.py после завершения
    пула потоков. Для desktop-инструмента это корректно — процесс всё равно
    скоро завершится.
    """
    import gc
    gc.collect()
    # После сборки мусора все COM-объекты, на которые нет ссылок,
    # будут освобождены автоматически. Но на случай, если win32com
    # держит внутренние ссылки, пытаемся Quit через pythoncom.
    try:
        import pythoncom  # type: ignore
        pythoncom.CoUninitialize()  # безопасно, даже если уже uninit
    except Exception:
        pass


class DocReader(BaseReader):
    name = "doc"

    def extract_text(self, path: Path, method: DocReadMethod = DocReadMethod.AUTO) -> ReaderOutcome:
        if not self.is_file_accessible(path):
            return ReaderOutcome(locked=True, error="Файл заблокирован")

        data = self.read_bytes(path)
        if data is None:
            return ReaderOutcome(error="Не удалось прочитать байты файла")
        if len(data) < 8:
            return ReaderOutcome(error="Файл слишком мал / повреждён")

        real_format = doc_formats.detect_format(data)

        # Форматы, которые не требуют внешних конвертеров — разбираем сразу
        if real_format == "ZIP":
            from app.readers.docx_reader import DocxReader
            outcome = DocxReader().extract_text(path)
            outcome.method_used = "docx-renamed"
            return outcome
        if real_format == "RTF":
            return self._wrap(doc_formats.read_rtf(data), "rtf-parser")
        if real_format == "HTML":
            return self._wrap(doc_formats.read_html(data), "html-parser")
        if real_format == "XML":
            return self._wrap(doc_formats.read_word_xml(data), "word2003-xml")

        # OLE2 или неизвестный формат — выбираем стратегию
        if method == DocReadMethod.HEURISTIC:
            return self._wrap(doc_formats.read_ole2_heuristic(data), "ole2-heuristic")
        if method == DocReadMethod.LIBREOFFICE:
            text = self._via_libreoffice(path)
            if text:
                return self._wrap(text, "libreoffice")
            return self._wrap(doc_formats.read_ole2_heuristic(data), "ole2-heuristic-fallback")
        if method == DocReadMethod.COM:
            text = self._via_com(path)
            if text:
                return self._wrap(text, "ms-word-com")
            return self._wrap(doc_formats.read_ole2_heuristic(data), "ole2-heuristic-fallback")

        # AUTO: LibreOffice -> COM -> эвристика
        text = self._via_libreoffice(path)
        if text and len(text.strip()) >= 5:
            return self._wrap(text, "libreoffice")

        if platform.system() == "Windows":
            text = self._via_com(path)
            if text and len(text.strip()) >= 5:
                return self._wrap(text, "ms-word-com")

        if real_format == "UNKNOWN":
            text = doc_formats.read_as_plain_text(data)
            if text:
                return self._wrap(text, "plain-text-fallback")

        return self._wrap(doc_formats.read_ole2_heuristic(data), "ole2-heuristic")

    # ------------------------------------------------------------------ #
    @staticmethod
    def _wrap(text: Optional[str], method: str) -> ReaderOutcome:
        if text and len(text.strip()) >= 1:
            return ReaderOutcome(text=text, method_used=method)
        return ReaderOutcome(error="Не удалось извлечь текст", method_used=method)

    # ------------------------------------------------------------------ #
    @staticmethod
    def _via_libreoffice(path: Path) -> Optional[str]:
        soffice = _find_libreoffice()
        if not soffice:
            return None
        timeout = load_settings().per_file_timeout
        try:
            with tempfile.TemporaryDirectory() as tmp_dir:
                result = subprocess.run(
                    [
                        soffice, "--headless", "--norestore", "--convert-to", "txt:Text",
                        "--outdir", tmp_dir, str(path),
                    ],
                    capture_output=True, timeout=timeout,
                )
                if result.returncode != 0:
                    return None
                out_file = Path(tmp_dir) / (path.stem + ".txt")
                if not out_file.exists():
                    return None
                return out_file.read_text(encoding="utf-8", errors="replace")
        except subprocess.TimeoutExpired:
            return None
        except Exception:
            return None

    # ------------------------------------------------------------------ #
    @staticmethod
    def _via_com(path: Path) -> Optional[str]:
        """Открывает файл через MS Word COM, извлекает текст, закрывает
        документ и Word. Каждый вызов — отдельный экземпляр Word, который
        гарантированно завершается, исключая утечки ресурсов при отмене
        сканирования или исключениях."""
        if platform.system() != "Windows":
            return None
        try:
            import win32com.client  # type: ignore
            import pythoncom  # type: ignore
        except ImportError:
            return None

        pythoncom.CoInitialize()
        word = None
        doc = None
        try:
            word = win32com.client.DispatchEx("Word.Application")
            word.Visible = False
            word.DisplayAlerts = 0
            doc = word.Documents.Open(str(path), False, True, False)
            text = doc.Content.Text
            return text
        except Exception:
            return None
        finally:
            if doc is not None:
                try:
                    doc.Close(False)
                except Exception:
                    pass
            if word is not None:
                try:
                    word.Quit()
                except Exception:
                    pass
            try:
                pythoncom.CoUninitialize()
            except Exception:
                pass
