from __future__ import annotations

import hashlib
from typing import Optional

import structlog

from export.parser import IDExportParser
from word.builder import DocxBuilder
from services.scan_service import ScanService
from repositories.conversion_repository import ConversionRepository
from models.conversion import ConversionResult
from core.exceptions import ConversionError, ExportParseError, InsufficientCreditsError
import core.storage as storage

log = structlog.get_logger()

# In-memory fallback for download when MinIO is not available
_conversion_results: dict = {}


class ConversionService:
    def __init__(
        self,
        scan_service: ScanService,
        conversion_repo: ConversionRepository,
    ):
        self.scan_service    = scan_service
        self.conversion_repo = conversion_repo
        self.parser          = IDExportParser()
        self.builder         = DocxBuilder()

    async def convert(
        self,
        session_id: str,
        filename: str = 'document',
        user: Optional[dict] = None,
        admin_token: str = '',
    ) -> ConversionResult:
        """Parse the cached export data and build a DOCX.

        If a user is provided, a PocketBase conversion record is created
        (triggering atomic credit deduction). On failure the record is marked
        failed, which triggers the credit refund hook.
        """
        cached = self.scan_service.get_cached(session_id)
        if not cached:
            raise ConversionError('Session expired or not found. Please re-upload your file.')

        data, scan_report = cached
        file_hash = _hash_data(data)

        # ── Credit deduction (if authenticated) ──────────────────────────────
        conversion_id: Optional[str] = None
        if user and admin_token:
            try:
                conversion_id = await self.conversion_repo.create(
                    user['id'], file_hash, admin_token
                )
            except Exception as e:
                error_str = str(e)
                if 'INSUFFICIENT_CREDITS' in error_str:
                    raise InsufficientCreditsError('You have no credits remaining.')
                log.error('conversion_record_create_failed', error=error_str)
                raise ConversionError('Failed to register conversion. Please try again.')

        # ── Build DOCX ───────────────────────────────────────────────────────
        try:
            document = self.parser.parse(data)
        except Exception as e:
            log.error('export_parse_failed', error=str(e))
            await self._mark_failed(conversion_id, admin_token)
            raise ExportParseError(f'Failed to parse export: {e}')

        try:
            docx_bytes = self.builder.build(document)
        except Exception as e:
            log.error('docx_build_failed', error=str(e))
            await self._mark_failed(conversion_id, admin_token)
            raise ConversionError(f'Failed to build DOCX: {e}')

        output_filename = (
            filename.replace('.json', '').replace('idconvert_export', 'document') + '.docx'
        )

        # ── Upload to MinIO or fall back to in-memory ─────────────────────────
        download_url = await self._store(session_id, docx_bytes, output_filename)

        if conversion_id and admin_token:
            await self.conversion_repo.mark_complete(conversion_id, download_url, admin_token)

        _conversion_results[session_id] = (docx_bytes, output_filename)

        return ConversionResult(download_url=download_url, filename=output_filename)

    async def _store(self, session_id: str, docx_bytes: bytes, filename: str) -> str:
        """Upload to MinIO and return presigned URL; fall back to local endpoint."""
        try:
            object_key = f'conversions/{session_id}/{filename}'
            return storage.upload_docx(object_key, docx_bytes)
        except Exception as e:
            log.warning('minio_unavailable_fallback', error=str(e))
            return f'/api/download/{session_id}'

    async def _mark_failed(self, conversion_id: Optional[str], admin_token: str) -> None:
        if conversion_id and admin_token:
            try:
                await self.conversion_repo.mark_failed(conversion_id, admin_token)
            except Exception as e:
                log.error('mark_failed_error', error=str(e))


def get_conversion_result(session_id: str):
    """Return (docx_bytes, filename) from in-memory store, or None."""
    return _conversion_results.get(session_id)


def _hash_data(data: dict) -> str:
    import json
    raw = json.dumps(data, sort_keys=True).encode()
    return hashlib.sha256(raw).hexdigest()
