from __future__ import annotations

import hashlib
import mimetypes
import os
import tempfile
from dataclasses import dataclass
from typing import Any, Callable
from urllib.parse import urlparse
from urllib.request import Request, urlopen

from django.conf import settings
from django.core.files import File
from django.core.files.storage import default_storage
from django.utils import timezone

from api_v1.mdy_plate_order_staging_tiia_upload import (
    _extract_plate_image_attachment_items,
    _pick_attachment_url,
)
from api_v1.models import (
    MDYPlateOrderStaging,
    MDYPlateOrderStagingQiniuImageUploadAudit,
    MDYPlateOrderStagingQiniuImageUploadState,
)
from api_v1.utils.tencentcloud_tiia import SimpleRateLimiter


RAW_PLATE_IMAGES_SOURCE = MDYPlateOrderStagingQiniuImageUploadAudit.Source.RAW_PLATE_IMAGES
DEFAULT_QINIU_PREFIX = 'mdy_plate_order_staging/plate_images'
DEFAULT_DOWNLOAD_CHUNK_SIZE = 1024 * 1024
DEFAULT_SPOOL_MAX_SIZE = 8 * 1024 * 1024
DEFAULT_MAX_IMAGE_SIZE = 100 * 1024 * 1024
DEFAULT_DOWNLOAD_TIMEOUT = 30
DEFAULT_UPLOAD_STATE_NAME = 'raw_plate_images_qiniu_upload'


@dataclass(frozen=True)
class PlateImageWorkItem:
    staging: MDYPlateOrderStaging
    attachment_index: int
    source_field: str
    original_url: str
    original_host: str
    raw_attachment: dict[str, Any]
    source: str = RAW_PLATE_IMAGES_SOURCE


@dataclass(frozen=True)
class UploadedImage:
    qiniu_key: str
    qiniu_url: str
    file_size: int
    content_type: str


@dataclass(frozen=True)
class ProcessResult:
    action: str
    audit: MDYPlateOrderStagingQiniuImageUploadAudit | None = None
    reason: str = ''


UploadFunc = Callable[..., UploadedImage]


def iter_plate_image_work_items(staging: MDYPlateOrderStaging):
    for idx, item in enumerate(_extract_plate_image_attachment_items(staging)):
        picked = _pick_attachment_url(item)
        if not picked.url:
            continue
        parsed = urlparse(picked.url)
        yield PlateImageWorkItem(
            staging=staging,
            attachment_index=idx,
            source_field=picked.source_field or '',
            original_url=picked.url,
            original_host=parsed.hostname or '',
            raw_attachment=dict(item),
        )


def is_qiniu_url(url: str) -> bool:
    configured_domain = getattr(settings, 'QINIU_BUCKET_DOMAIN', '') or ''
    if not configured_domain:
        return False
    return (urlparse(url).hostname or '').lower() == configured_domain.lower()


def build_qiniu_key(work_item: PlateImageWorkItem, *, content_type: str = '', prefix: str = DEFAULT_QINIU_PREFIX) -> str:
    digest = hashlib.sha256(work_item.original_url.encode('utf-8')).hexdigest()
    ext = _pick_extension(work_item.original_url, content_type)
    return (
        f'{prefix}/staging-{work_item.staging.id}/'
        f'attachment-{work_item.attachment_index}-{digest[:32]}{ext}'
    )


def get_or_create_audit_for_work_item(work_item: PlateImageWorkItem):
    return MDYPlateOrderStagingQiniuImageUploadAudit.objects.get_or_create(
        staging=work_item.staging,
        source=work_item.source,
        attachment_index=work_item.attachment_index,
        source_field=work_item.source_field,
        original_url=work_item.original_url,
        defaults={
            'mdy_rowid': work_item.staging.mdy_rowid,
            'original_host': work_item.original_host,
            'raw_attachment': work_item.raw_attachment,
        },
    )


def get_or_create_default_upload_state():
    return MDYPlateOrderStagingQiniuImageUploadState.objects.get_or_create(
        name=DEFAULT_UPLOAD_STATE_NAME,
    )[0]


def build_work_item_from_audit(audit: MDYPlateOrderStagingQiniuImageUploadAudit) -> PlateImageWorkItem:
    return PlateImageWorkItem(
        staging=audit.staging,
        attachment_index=audit.attachment_index,
        source_field=audit.source_field,
        original_url=audit.original_url,
        original_host=audit.original_host,
        raw_attachment=audit.raw_attachment if isinstance(audit.raw_attachment, dict) else {},
        source=audit.source,
    )


def should_process_audit(
    audit: MDYPlateOrderStagingQiniuImageUploadAudit,
    *,
    retry_failed: bool = False,
    retry_stale_processing: bool = False,
    stale_before=None,
    max_attempts: int | None = None,
) -> tuple[bool, str]:
    status = audit.status
    statuses = MDYPlateOrderStagingQiniuImageUploadAudit.Status

    if status == statuses.SUCCESS:
        return False, 'already_success'
    if status == statuses.SKIPPED:
        return False, 'already_skipped'
    if max_attempts is not None and audit.attempts >= max_attempts:
        return False, 'max_attempts_reached'
    if status == statuses.FAILED and not retry_failed:
        return False, 'failed_needs_retry_failed'
    if status == statuses.PROCESSING:
        if not retry_stale_processing:
            return False, 'processing'
        if stale_before is not None and audit.last_attempt_at and audit.last_attempt_at > stale_before:
            return False, 'processing_not_stale'
    return True, ''


def process_plate_image_work_item(
    work_item: PlateImageWorkItem,
    *,
    retry_failed: bool = False,
    retry_stale_processing: bool = False,
    stale_before=None,
    max_attempts: int | None = None,
    rate_limiter: SimpleRateLimiter | None = None,
    upload_func: UploadFunc | None = None,
    max_image_size: int = DEFAULT_MAX_IMAGE_SIZE,
    spool_max_size: int = DEFAULT_SPOOL_MAX_SIZE,
    chunk_size: int = DEFAULT_DOWNLOAD_CHUNK_SIZE,
    timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
) -> ProcessResult:
    audit, _created = get_or_create_audit_for_work_item(work_item)
    return process_qiniu_image_upload_audit(
        audit,
        work_item=work_item,
        retry_failed=retry_failed,
        retry_stale_processing=retry_stale_processing,
        stale_before=stale_before,
        max_attempts=max_attempts,
        rate_limiter=rate_limiter,
        upload_func=upload_func,
        max_image_size=max_image_size,
        spool_max_size=spool_max_size,
        chunk_size=chunk_size,
        timeout=timeout,
    )


def process_qiniu_image_upload_audit(
    audit: MDYPlateOrderStagingQiniuImageUploadAudit,
    *,
    work_item: PlateImageWorkItem | None = None,
    retry_failed: bool = False,
    retry_stale_processing: bool = False,
    stale_before=None,
    max_attempts: int | None = None,
    rate_limiter: SimpleRateLimiter | None = None,
    upload_func: UploadFunc | None = None,
    max_image_size: int = DEFAULT_MAX_IMAGE_SIZE,
    spool_max_size: int = DEFAULT_SPOOL_MAX_SIZE,
    chunk_size: int = DEFAULT_DOWNLOAD_CHUNK_SIZE,
    timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
) -> ProcessResult:
    if work_item is None:
        work_item = build_work_item_from_audit(audit)
    can_process, reason = should_process_audit(
        audit,
        retry_failed=retry_failed,
        retry_stale_processing=retry_stale_processing,
        stale_before=stale_before,
        max_attempts=max_attempts,
    )
    if not can_process:
        return ProcessResult(action='skipped', audit=audit, reason=reason)

    if is_qiniu_url(work_item.original_url):
        _mark_audit_skipped(audit, reason='original_url_already_qiniu')
        return ProcessResult(action='skipped', audit=audit, reason='original_url_already_qiniu')

    _mark_audit_processing(audit)

    try:
        reused = _find_reusable_success_audit(audit)
        if reused is not None:
            _mark_audit_success(
                audit,
                qiniu_key=reused.qiniu_key,
                qiniu_url=reused.qiniu_url,
                file_size=reused.file_size,
                content_type=reused.content_type,
            )
            return ProcessResult(action='reused', audit=audit)

        if rate_limiter is not None:
            rate_limiter.wait()
        uploader = upload_func or upload_original_url_to_qiniu
        uploaded = uploader(
            work_item,
            max_image_size=max_image_size,
            spool_max_size=spool_max_size,
            chunk_size=chunk_size,
            timeout=timeout,
        )
        _mark_audit_success(
            audit,
            qiniu_key=uploaded.qiniu_key,
            qiniu_url=uploaded.qiniu_url,
            file_size=uploaded.file_size,
            content_type=uploaded.content_type,
        )
        return ProcessResult(action='uploaded', audit=audit)
    except Exception as exc:
        _mark_audit_failed(audit, str(exc))
        return ProcessResult(action='failed', audit=audit, reason=str(exc))


def upload_original_url_to_qiniu(
    work_item: PlateImageWorkItem,
    *,
    max_image_size: int = DEFAULT_MAX_IMAGE_SIZE,
    spool_max_size: int = DEFAULT_SPOOL_MAX_SIZE,
    chunk_size: int = DEFAULT_DOWNLOAD_CHUNK_SIZE,
    timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
) -> UploadedImage:
    request = Request(
        work_item.original_url,
        headers={'User-Agent': 'flower-mdy-plate-image-qiniu-migration/1.0'},
    )
    with urlopen(request, timeout=timeout) as response:
        status = getattr(response, 'status', 200)
        if status >= 400:
            raise RuntimeError(f'download failed with status {status}')

        content_type = response.headers.get('Content-Type', '').split(';', 1)[0].strip()
        content_length = _parse_content_length(response.headers.get('Content-Length'))
        if content_length is not None and content_length > max_image_size:
            raise RuntimeError(f'image too large: {content_length} > {max_image_size}')

        qiniu_key = build_qiniu_key(work_item, content_type=content_type)
        with tempfile.SpooledTemporaryFile(max_size=spool_max_size, mode='w+b') as tmp:
            total = 0
            while True:
                chunk = response.read(chunk_size)
                if not chunk:
                    break
                total += len(chunk)
                if total > max_image_size:
                    raise RuntimeError(f'image too large: {total} > {max_image_size}')
                tmp.write(chunk)

            tmp.seek(0)
            saved_key = default_storage.save(qiniu_key, File(tmp, name=os.path.basename(qiniu_key)))
            return UploadedImage(
                qiniu_key=saved_key,
                qiniu_url=default_storage.url(saved_key),
                file_size=total,
                content_type=content_type,
            )


def build_default_qiniu_rate_limiter(qps: float | None = None) -> SimpleRateLimiter | None:
    effective_qps = qps
    if effective_qps is None:
        effective_qps = float(getattr(settings, 'MDY_PLATE_ORDER_STAGING_QINIU_UPLOAD_QPS', 2) or 2)
    if effective_qps <= 0:
        return None
    return SimpleRateLimiter(qps=effective_qps)


def _find_reusable_success_audit(
    audit: MDYPlateOrderStagingQiniuImageUploadAudit,
) -> MDYPlateOrderStagingQiniuImageUploadAudit | None:
    statuses = MDYPlateOrderStagingQiniuImageUploadAudit.Status
    return (
        MDYPlateOrderStagingQiniuImageUploadAudit.objects.filter(
            original_url=audit.original_url,
            status=statuses.SUCCESS,
        )
        .exclude(pk=audit.pk)
        .exclude(qiniu_url='')
        .order_by('id')
        .first()
    )


def _mark_audit_processing(audit: MDYPlateOrderStagingQiniuImageUploadAudit) -> None:
    now = timezone.now()
    audit.status = MDYPlateOrderStagingQiniuImageUploadAudit.Status.PROCESSING
    audit.attempts += 1
    audit.error = ''
    audit.started_at = now
    audit.last_attempt_at = now
    audit.save(update_fields=['status', 'attempts', 'error', 'started_at', 'last_attempt_at', 'updated_at'])


def _mark_audit_success(
    audit: MDYPlateOrderStagingQiniuImageUploadAudit,
    *,
    qiniu_key: str,
    qiniu_url: str,
    file_size: int | None,
    content_type: str,
) -> None:
    now = timezone.now()
    audit.status = MDYPlateOrderStagingQiniuImageUploadAudit.Status.SUCCESS
    audit.qiniu_key = qiniu_key
    audit.qiniu_url = qiniu_url
    audit.file_size = file_size
    audit.content_type = content_type or ''
    audit.error = ''
    audit.uploaded_at = now
    audit.save(
        update_fields=[
            'status',
            'qiniu_key',
            'qiniu_url',
            'file_size',
            'content_type',
            'error',
            'uploaded_at',
            'updated_at',
        ]
    )


def _mark_audit_failed(audit: MDYPlateOrderStagingQiniuImageUploadAudit, error: str) -> None:
    audit.status = MDYPlateOrderStagingQiniuImageUploadAudit.Status.FAILED
    audit.error = error[:4000]
    audit.save(update_fields=['status', 'error', 'updated_at'])


def _mark_audit_skipped(audit: MDYPlateOrderStagingQiniuImageUploadAudit, *, reason: str) -> None:
    audit.status = MDYPlateOrderStagingQiniuImageUploadAudit.Status.SKIPPED
    audit.error = reason
    audit.save(update_fields=['status', 'error', 'updated_at'])


def _parse_content_length(value: str | None) -> int | None:
    if not value:
        return None
    try:
        return int(value)
    except ValueError:
        return None


def _pick_extension(url: str, content_type: str = '') -> str:
    path = urlparse(url).path
    ext = os.path.splitext(path)[1].lower()
    if ext and len(ext) <= 10:
        return ext
    guessed = mimetypes.guess_extension(content_type) if content_type else None
    return guessed or ''
