from __future__ import annotations

import ipaddress
import logging
import re
import socket
import uuid
from collections.abc import Sequence
from typing import TYPE_CHECKING
from urllib.parse import urlparse

from amsdal_models.classes.model import Model
from amsdal_utils.models.data_models.reference import Reference

if TYPE_CHECKING:
    import aiohttp


def clean_json_response(response: str) -> str:
    response = response.strip()

    if '```json' in response:
        return response.split('```json')[1].split('```')[0].strip()

    if '```' in response:
        return response.split('```')[1].split('```')[0].strip()

    return response


def convert_ref_dict_to_reference(
    ref_dict: dict[str, object],
    *,
    logger: logging.Logger | None = None,
) -> object:
    log = logger or logging.getLogger(__name__)

    raw_ref = ref_dict.get('$ref', ref_dict)
    if not isinstance(raw_ref, dict):
        log.warning('Invalid reference format: %s', ref_dict)
        return ref_dict

    class_name = raw_ref.get('class_name')
    object_id = raw_ref.get('object_id')
    resource = raw_ref.get('resource') or class_name

    if class_name is None or object_id is None or resource is None:
        log.warning('Missing required fields in reference: %s', ref_dict)
        return ref_dict

    ref_payload = {
        'resource': resource,
        'class_name': class_name,
        'class_version': raw_ref.get('class_version', 'LATEST'),
        'object_id': object_id,
        'object_version': raw_ref.get('object_version', 'LATEST'),
    }

    result = Reference(ref=ref_payload)
    log.debug('Created reference: %s', result)
    return result


async def download_file_from_spec(
    file_spec: dict[str, object],
    *,
    logger: logging.Logger | None = None,
    session: aiohttp.ClientSession | None = None,
    max_size: int = 10 * 1024 * 1024,
    allowed_mime_types: Sequence[str] | None = None,
) -> object:
    log = logger or logging.getLogger(__name__)
    url = file_spec.get('source_url')

    if not isinstance(url, str) or not url:
        return file_spec

    try:
        import aiohttp
        import magic
        from amsdal.models.core.file import File
    except Exception as exc:  # noqa: BLE001
        log.error('Failed to initialize downloader for %s: %s', url, exc)
        return file_spec

    parsed = urlparse(url)
    if parsed.scheme not in {'http', 'https'}:
        log.warning('Blocked non-http(s) URL: %s', url)
        return file_spec

    if parsed.username or parsed.password:
        log.warning('Blocked URL with credentials: %s', url)
        return file_spec

    hostname = parsed.hostname
    if not hostname or not _is_safe_hostname(hostname):
        log.warning('Blocked unsafe host: %s', url)
        return file_spec

    log.debug('Downloading file from %s', url)

    own_session = session is None
    if own_session:
        timeout = aiohttp.ClientTimeout(total=30)
        session = aiohttp.ClientSession(timeout=timeout)

    if session is None:
        log.error('Failed to initialize HTTP session for %s', url)
        return file_spec

    try:
        async with session.get(url, allow_redirects=False) as response:
            if response.status in {301, 302, 303, 307, 308}:
                log.warning('Blocked redirect from %s', url)
                return file_spec

            response.raise_for_status()

            content_length = response.headers.get('Content-Length')
            if content_length and int(content_length) > max_size:
                log.warning('Blocked large file (%s bytes) from %s', content_length, url)
                return file_spec

            chunks: list[bytes] = []
            size = 0

            first_chunk = await response.content.read(4096)
            if first_chunk:
                chunks.append(first_chunk)
                size += len(first_chunk)

            mime_type = None
            if first_chunk:
                try:
                    mime_type = magic.from_buffer(first_chunk, mime=True)
                except Exception as exc:  # noqa: BLE001
                    log.warning('Failed to detect MIME type for %s: %s', url, exc)

            allowed_types = _normalize_allowed_types(allowed_mime_types)
            if mime_type and allowed_types and mime_type not in allowed_types:
                log.warning('Blocked file with MIME type %s from %s', mime_type, url)
                return file_spec

            async for chunk in response.content.iter_chunked(8192):
                if not chunk:
                    continue
                size += len(chunk)
                if size > max_size:
                    log.warning('Blocked file exceeding max_size (%s bytes) from %s', max_size, url)
                    return file_spec
                chunks.append(chunk)

        content = b''.join(chunks)
        filename = _build_safe_filename(url, mime_type)
        file_obj = File(filename=filename, data=content)
        log.debug('Created File object for %s, size: %s bytes', filename, len(content))
        return file_obj
    except Exception as exc:  # noqa: BLE001
        log.error('Failed to download file from %s: %s', url, exc)
        return file_spec
    finally:
        if own_session and session is not None:
            await session.close()


def _build_safe_filename(url: str, mime_type: str | None) -> str:
    parsed = urlparse(url)
    raw_name = (parsed.path.rsplit('/', 1)[-1] or '').strip()
    safe_name = _sanitize_filename(raw_name)
    if not safe_name:
        safe_name = uuid.uuid4().hex

    if '.' not in safe_name and mime_type:
        ext = _extension_from_mime(mime_type)
        if ext:
            safe_name = f'{safe_name}{ext}'

    return safe_name


def _sanitize_filename(name: str) -> str:
    name = name.replace('\x00', '')
    name = name.split('?')[0].split('#')[0]
    name = name.rsplit('/', 1)[-1]
    name = re.sub(r'[^A-Za-z0-9._-]+', '_', name)
    name = name.strip('._')
    return name


def _extension_from_mime(mime_type: str) -> str | None:
    mapping = {
        'image/jpeg': '.jpg',
        'image/png': '.png',
        'image/gif': '.gif',
        'image/webp': '.webp',
        'application/pdf': '.pdf',
        'text/plain': '.txt',
    }
    return mapping.get(mime_type)


def _normalize_allowed_types(allowed: Sequence[str] | None) -> set[str]:
    if allowed is None:
        return {
            'image/jpeg',
            'image/png',
            'image/gif',
            'image/webp',
            'application/pdf',
            'text/plain',
        }
    return {value.strip().lower() for value in allowed if isinstance(value, str) and value.strip()}


def _is_safe_hostname(hostname: str) -> bool:
    lowered = hostname.lower().strip('.')
    if lowered in {'localhost', 'localhost.localdomain'}:
        return False
    if lowered.endswith('.local') or lowered.endswith('.localhost'):
        return False

    try:
        ip = ipaddress.ip_address(lowered)
        return _is_public_ip(ip)
    except ValueError:
        return _is_public_host(lowered)


def _is_public_host(hostname: str) -> bool:
    try:
        infos = socket.getaddrinfo(hostname, None)
    except OSError:
        return False

    addresses = {item[4][0] for item in infos if item and item[4]}
    if not addresses:
        return False

    for addr in addresses:
        try:
            ip = ipaddress.ip_address(addr)
        except ValueError:
            return False
        if not _is_public_ip(ip):
            return False

    return True


def _is_public_ip(ip: ipaddress.IPv4Address | ipaddress.IPv6Address) -> bool:
    return not (ip.is_private or ip.is_loopback or ip.is_reserved or ip.is_link_local or ip.is_multicast)


def _is_file_payload(value: dict[str, object]) -> bool:
    if 'data' not in value:
        return False
    return any(key in value for key in ('filename', 'size', 'storage_address'))


def _extract_file_bytes(value: object) -> tuple[bytes | bytearray | None, str | None]:
    if isinstance(value, (bytes, bytearray)):
        return value, None

    data = getattr(value, 'data', None)
    if isinstance(data, (bytes, bytearray)):
        name = getattr(value, 'filename', None)
        if not isinstance(name, str):
            name = None
        return data, name

    return None, None


async def process_payload_fields(
    fields: dict[str, object],
    *,
    model_class: type[Model] | None = None,
    logger: logging.Logger | None = None,
    session: aiohttp.ClientSession | None = None,
    max_size: int = 10 * 1024 * 1024,
    allowed_mime_types: Sequence[str] | None = None,
) -> dict[str, object]:
    log = logger or logging.getLogger(__name__)
    processed: dict[str, object] = {}

    for key, value in fields.items():
        if isinstance(value, dict):
            if '$ref' in value:
                processed[key] = convert_ref_dict_to_reference(value, logger=log)
                continue

            if _is_file_payload(value):
                file_payload = dict(value)
                data_value = file_payload.get('data')

                if isinstance(data_value, dict) and 'source_url' in data_value:
                    downloaded = await download_file_from_spec(
                        data_value,
                        logger=log,
                        session=session,
                        max_size=max_size,
                        allowed_mime_types=allowed_mime_types,
                    )
                    file_bytes, file_name = _extract_file_bytes(downloaded)
                    if file_bytes is not None:
                        file_payload['data'] = file_bytes
                        file_payload['size'] = len(file_bytes)
                        if not file_payload.get('filename') and file_name:
                            file_payload['filename'] = file_name
                        processed[key] = file_payload
                        continue

                file_bytes, file_name = _extract_file_bytes(data_value)
                if file_bytes is not None:
                    file_payload['data'] = file_bytes
                    file_payload['size'] = len(file_bytes)
                    if not file_payload.get('filename') and file_name:
                        file_payload['filename'] = file_name
                    processed[key] = file_payload
                    continue

            processed[key] = await process_payload_fields(
                value,
                model_class=model_class,
                logger=log,
                session=session,
                max_size=max_size,
                allowed_mime_types=allowed_mime_types,
            )
            continue

        if isinstance(value, list):
            out_list: list[object] = []
            for item in value:
                if isinstance(item, dict):
                    out_list.append(
                        await process_payload_fields(
                            item,
                            model_class=model_class,
                            logger=log,
                            session=session,
                            max_size=max_size,
                            allowed_mime_types=allowed_mime_types,
                        )
                    )
                else:
                    out_list.append(item)
            processed[key] = out_list
            continue

        processed[key] = value

    return processed
