import re
import uuid

from fastapi import HTTPException, UploadFile, status
from sqlalchemy.orm import Session

from app.models.knowledge import KnowledgeDevice, KnowledgeDocument, KnowledgeManufacturer, KnowledgeNote
from app.repositories.knowledge_repository import KnowledgeRepository
from app.schemas.knowledge import (
    KnowledgeDeviceCreate,
    KnowledgeDeviceUpdate,
    KnowledgeDocumentCreate,
    KnowledgeDocumentUpdate,
    KnowledgeManufacturerCreate,
    KnowledgeManufacturerUpdate,
    KnowledgeNoteCreate,
    KnowledgeNoteUpdate,
)
from app.storage import get_storage_service
from app.storage.exceptions import StorageFileNotFoundError, StorageValidationError


def slugify(value: str) -> str:
    normalized = value.strip().lower()
    normalized = normalized.replace("ä", "ae").replace("ö", "oe").replace("ü", "ue").replace("ß", "ss")
    normalized = re.sub(r"[^a-z0-9]+", "-", normalized)
    normalized = normalized.strip("-")
    return normalized or uuid.uuid4().hex[:10]


def unique_slug(db: Session, base: str, exists) -> str:
    slug = slugify(base)
    candidate = slug
    index = 2
    while exists(db, candidate):
        candidate = f"{slug}-{index}"
        index += 1
    return candidate


def parse_tags(value: str) -> list[str]:
    seen: set[str] = set()
    tags: list[str] = []
    for item in value.split(","):
        tag = item.strip()
        key = tag.lower()
        if tag and key not in seen:
            seen.add(key)
            tags.append(tag)
    return tags


def storage_validation_error(exc: StorageValidationError) -> HTTPException:
    detail = str(exc) or "Ungültige Datei"
    status_code = status.HTTP_413_REQUEST_ENTITY_TOO_LARGE if "groß" in detail else status.HTTP_400_BAD_REQUEST
    return HTTPException(status_code=status_code, detail=detail)


class KnowledgeService:
    @staticmethod
    def create_manufacturer(db: Session, payload: KnowledgeManufacturerCreate) -> KnowledgeManufacturer:
        if KnowledgeRepository.get_manufacturer_by_name(db, payload.name):
            raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Herstellername ist bereits vergeben")
        slug = unique_slug(db, payload.name, lambda session, value: KnowledgeRepository.get_manufacturer_by_slug(session, value) is not None)
        manufacturer = KnowledgeManufacturer(name=payload.name, slug=slug, website=str(payload.website or ""), notes=payload.notes)
        db.add(manufacturer)
        db.commit()
        db.refresh(manufacturer)
        return manufacturer

    @staticmethod
    def update_manufacturer(db: Session, manufacturer: KnowledgeManufacturer, payload: KnowledgeManufacturerUpdate) -> KnowledgeManufacturer:
        if KnowledgeRepository.get_manufacturer_by_name(db, payload.name, manufacturer.id):
            raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Herstellername ist bereits vergeben")
        manufacturer.name = payload.name
        manufacturer.slug = unique_slug(
            db,
            payload.name,
            lambda session, value: KnowledgeRepository.get_manufacturer_by_slug(session, value, manufacturer.id) is not None,
        )
        manufacturer.website = str(payload.website or "")
        manufacturer.notes = payload.notes
        db.commit()
        db.refresh(manufacturer)
        return manufacturer

    @staticmethod
    def create_device(db: Session, payload: KnowledgeDeviceCreate) -> KnowledgeDevice:
        if KnowledgeRepository.get_manufacturer(db, payload.manufacturer_id) is None:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Hersteller nicht gefunden")
        slug = slugify(payload.model_number or payload.name)
        if KnowledgeRepository.get_device_by_slug(db, manufacturer_id=payload.manufacturer_id, slug=slug):
            raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Geräte-Slug ist für diesen Hersteller bereits vergeben")
        device = KnowledgeDevice(slug=slug, **payload.model_dump())
        db.add(device)
        db.commit()
        return KnowledgeRepository.get_device(db, device.id) or device

    @staticmethod
    def update_device(db: Session, device: KnowledgeDevice, payload: KnowledgeDeviceUpdate) -> KnowledgeDevice:
        if KnowledgeRepository.get_manufacturer(db, payload.manufacturer_id) is None:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Hersteller nicht gefunden")
        slug = slugify(payload.model_number or payload.name)
        if KnowledgeRepository.get_device_by_slug(db, manufacturer_id=payload.manufacturer_id, slug=slug, exclude_id=device.id):
            raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Geräte-Slug ist für diesen Hersteller bereits vergeben")
        for key, value in payload.model_dump().items():
            setattr(device, key, value)
        device.slug = slug
        db.commit()
        return KnowledgeRepository.get_device(db, device.id) or device

    @staticmethod
    def create_document(db: Session, payload: KnowledgeDocumentCreate) -> KnowledgeDocument:
        KnowledgeService._validate_document_links(db, payload.manufacturer_id, payload.device_id)
        slug = unique_slug(db, payload.title, lambda session, value: KnowledgeRepository.get_document_by_slug(session, value) is not None)
        document = KnowledgeDocument(slug=slug, external_url=str(payload.external_url or ""), **payload.model_dump(exclude={"external_url"}))
        db.add(document)
        db.commit()
        return KnowledgeRepository.get_document(db, document.id) or document

    @staticmethod
    async def upload_document(
        db: Session,
        *,
        file: UploadFile,
        payload: KnowledgeDocumentCreate,
    ) -> KnowledgeDocument:
        KnowledgeService._validate_upload_links(db, payload.manufacturer_id, payload.device_id)
        storage_service = get_storage_service()
        max_bytes = storage_service.max_upload_mb * 1024 * 1024
        content = await file.read(max_bytes + 1)
        original_name = file.filename or ""
        mime_type = file.content_type
        try:
            metadata = storage_service.save_file(
                namespace=f"knowledge/documents/{payload.manufacturer_id}",
                content=content,
                original_filename=original_name,
                mime_type=mime_type,
            )
        except StorageValidationError as exc:
            raise storage_validation_error(exc) from exc
        slug = unique_slug(db, payload.title, lambda session, value: KnowledgeRepository.get_document_by_slug(session, value) is not None)
        document = KnowledgeDocument(
            slug=slug,
            external_url=str(payload.external_url or ""),
            file_name=metadata.original_filename,
            file_path=metadata.storage_key,
            mime_type=metadata.mime_type,
            file_size=metadata.size,
            checksum_sha256=metadata.checksum_sha256,
            **payload.model_dump(exclude={"external_url"}),
        )
        db.add(document)
        db.commit()
        return KnowledgeRepository.get_document(db, document.id) or document

    @staticmethod
    def update_document(db: Session, document: KnowledgeDocument, payload: KnowledgeDocumentUpdate) -> KnowledgeDocument:
        KnowledgeService._validate_document_links(db, payload.manufacturer_id, payload.device_id)
        slug = unique_slug(db, payload.title, lambda session, value: KnowledgeRepository.get_document_by_slug(session, value, document.id) is not None)
        for key, value in payload.model_dump(exclude={"external_url"}).items():
            setattr(document, key, value)
        document.external_url = str(payload.external_url or "")
        document.slug = slug
        db.commit()
        return KnowledgeRepository.get_document(db, document.id) or document

    @staticmethod
    def create_note(db: Session, payload: KnowledgeNoteCreate) -> KnowledgeNote:
        KnowledgeService._validate_optional_links(db, payload.manufacturer_id, payload.device_id)
        note = KnowledgeNote(**payload.model_dump())
        db.add(note)
        db.commit()
        return KnowledgeRepository.get_note(db, note.id) or note

    @staticmethod
    def open_document_file(document: KnowledgeDocument):
        if not document.file_path:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Dokument hat keine lokale Datei")
        try:
            return get_storage_service().open_file(document.file_path)
        except StorageFileNotFoundError as exc:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Datei nicht gefunden") from exc
        except StorageValidationError as exc:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Ungültiger Dateipfad") from exc

    @staticmethod
    def delete_document_file(document: KnowledgeDocument) -> None:
        if not document.file_path:
            return
        KnowledgeService.delete_storage_key(document.file_path)

    @staticmethod
    def delete_storage_key(storage_key: str) -> None:
        try:
            get_storage_service().delete_file(storage_key)
        except StorageValidationError as exc:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Ungültiger Dateipfad") from exc

    @staticmethod
    def update_note(db: Session, note: KnowledgeNote, payload: KnowledgeNoteUpdate) -> KnowledgeNote:
        KnowledgeService._validate_optional_links(db, payload.manufacturer_id, payload.device_id)
        for key, value in payload.model_dump().items():
            setattr(note, key, value)
        db.commit()
        return KnowledgeRepository.get_note(db, note.id) or note

    @staticmethod
    def _validate_document_links(db: Session, manufacturer_id: int, device_id: int | None) -> None:
        if not manufacturer_id:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Bitte wählen Sie einen Hersteller aus.")
        if KnowledgeRepository.get_manufacturer(db, manufacturer_id) is None:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Hersteller nicht gefunden")
        if device_id is not None:
            device = KnowledgeRepository.get_device(db, device_id)
            if device is None:
                raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Gerät nicht gefunden")
            if device.manufacturer_id != manufacturer_id:
                raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Gerät gehört nicht zum Hersteller")

    @staticmethod
    def _validate_optional_links(db: Session, manufacturer_id: int | None, device_id: int | None) -> None:
        if manufacturer_id is not None and KnowledgeRepository.get_manufacturer(db, manufacturer_id) is None:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Hersteller nicht gefunden")
        if device_id is not None:
            device = KnowledgeRepository.get_device(db, device_id)
            if device is None:
                raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Gerät nicht gefunden")
            if manufacturer_id is not None and device.manufacturer_id != manufacturer_id:
                raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Gerät gehört nicht zum Hersteller")

    @staticmethod
    def _validate_upload_links(db: Session, manufacturer_id: int, device_id: int | None) -> None:
        if not manufacturer_id:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Bitte wählen Sie einen Hersteller aus.")
        if device_id is None:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Bitte wählen Sie ein Gerät aus.")

        manufacturer = KnowledgeRepository.get_manufacturer(db, manufacturer_id)
        if manufacturer is None:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Der ausgewählte Hersteller existiert nicht.")

        device = KnowledgeRepository.get_device(db, device_id)
        if device is None:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Das ausgewählte Gerät existiert nicht.")
        if device.manufacturer_id != manufacturer_id:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="Das ausgewählte Gerät gehört nicht zum ausgewählten Hersteller.",
            )
