import asyncio
import io
from datetime import date, datetime
from typing import Optional

from openpyxl import Workbook
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession

from src.repository.budget_form_repository import BudgetFormRepository
from src.core.errors import AccessDeniedException, ValidationException
from src.db.models import AppUser, UserRoleEnum, Vsp
from src.domain.schemas import VSPCreate, VSPUpdate
from src.repository.org_unit_repository import OrgUnitRepository
from src.repository.user_repository import UserRepository
from src.repository.vsp_repository import VSPRepository

_last_info_close_sync_run: Optional[date] = None
_info_close_sync_lock = asyncio.Lock()


class VSPService:
    EXECUTOR_EDITABLE_FIELDS = {
        "placement_type",
        "staff_count",
        "total_area",
        "rent_contract_num",
        "rent_end_date",
    }

    def __init__(self, db: AsyncSession):
        self.db = db
        self.vsp_repo = VSPRepository(db)
        self.form_repo = BudgetFormRepository(db)
        self.org_unit_repo = OrgUnitRepository(db)
        self.user_repo = UserRepository(db)

    async def get(self, vsp_id: int, user: AppUser, load_org_unit: bool = False) -> Optional[Vsp]:
        vsp = await self.vsp_repo.get(
            vsp_id=vsp_id, 
            load_org_unit=load_org_unit, 
            with_deleted=user.role_id == UserRoleEnum.ADMIN,
        )
        if not vsp:
            return None
        if user.role_id == UserRoleEnum.ADMIN:
            return vsp

        ssp_ids = await self._get_scoped_ssp_ids(user=user)
        if not ssp_ids or vsp.branch_id not in set(ssp_ids):
            raise AccessDeniedException()
        return vsp

    async def get_list(
        self,
        user: AppUser,
        registration_number: str | None = None,
        address: str | None = None,
        ssp_id: int | None = None,
        ssp_ids: list[int] | None = None,
        open_date_start: date | None = None,
        open_date_end: date | None = None,
        placement_type: str | None = None,
        staff_count_min: int | None = None,
        staff_count_max: int | None = None,
        is_active: bool | None = None,
        load_org_unit: bool = False,
        with_count: bool = False,
    ) -> list[Vsp] | tuple[int, list[Vsp]]:
        await self._sync_closed_records()
        scoped_ssp_ids = await self._get_scoped_ssp_ids(user=user)
        requested_ssp_ids = list(dict.fromkeys(ssp_ids or []))
        if ssp_id is not None and ssp_id not in requested_ssp_ids:
            requested_ssp_ids.append(ssp_id)

        effective_ssp_ids = scoped_ssp_ids
        if requested_ssp_ids:
            if scoped_ssp_ids is None:
                effective_ssp_ids = requested_ssp_ids
            else:
                scoped_set = set(scoped_ssp_ids)
                effective_ssp_ids = [item for item in requested_ssp_ids if item in scoped_set]
                if not effective_ssp_ids:
                    if with_count:
                        return 0, []
                    return []
        return await self.vsp_repo.get_list(
            registration_number=registration_number,
            address=address,
            ssp_ids=effective_ssp_ids,
            open_date_start=open_date_start,
            open_date_end=open_date_end,
            placement_type=placement_type,
            staff_count_min=staff_count_min,
            staff_count_max=staff_count_max,
            is_active=is_active,
            load_org_unit=load_org_unit,
            with_count=with_count,
        )

    async def get_dropdown(
            self, 
            user: AppUser, 
            ssp_id: int | None = None,
            form_id: int | None = None,
            with_count: bool = False,
    ) -> list[Vsp]:
        ssp_ids = await self._get_scoped_ssp_ids(user=user)
        
        checking_ssps = set()
        if ssp_id is not None:
            checking_ssps.add(ssp_id)
        if form_id is not None:
            form = await self.form_repo.get(budget_form_id=form_id)
            if form is not None:
                checking_ssps.add(form.org_unit_id)
        if checking_ssps:
            if ssp_ids is None:
                ssp_ids = list(checking_ssps)
            elif not checking_ssps & set(ssp_ids):
                if with_count:
                    return 0, []
                return []
            else:
                ssp_ids = list(checking_ssps & set(ssp_ids))

        await self._sync_closed_records()
        return await self.vsp_repo.get_list(
            ssp_ids=ssp_ids,
            is_active=True,
            with_count=with_count,
        )

    async def create(self, payload: VSPCreate, user: AppUser, load_org_unit: bool = False) -> Vsp:
        if not self._can_modify(user):
            raise AccessDeniedException()
        await self._ensure_active_branch(payload.branch_id)
        try:
            created = await self.vsp_repo.create(payload=payload, created_by=user.id, load_org_unit=load_org_unit)
            if created.closed_at is not None and created.closed_at <= date.today():
                created = await self.vsp_repo.update(
                    vsp=created,
                    data={"is_active": False},
                    updated_by=user.id,
                    load_org_unit=load_org_unit,
                )
            return created
        except IntegrityError as exc:
            message = str(getattr(exc, "orig", exc) or exc).strip()
            if "vsp_reg_number_unique" in message:
                raise ValidationException(description="Указанный номер уже существует")
            if "chk_closed_after_opened" in message:
                raise ValidationException(description="Дата закрытия не может быть меньше даты открытия")
            if "chk_rent_date" in message:
                raise ValidationException(description="Срок окончания договора аренды не может быть меньше даты открытия")
            raise ValidationException(description="Некорректные данные для записи ВСП") from exc
    
    async def export_xlsx(
        self,
        user: AppUser,
        registration_number: str | None = None,
        address: str | None = None,
        ssp_ids: list[int] | None = None,
        open_date_start: date | None = None,
        open_date_end: date | None = None,
        placement_type: str | None = None,
        staff_count_min: int | None = None,
        staff_count_max: int | None = None,
    ) -> tuple[io.BytesIO, str]:
        data = await self.get_list(
            user=user,
            registration_number=registration_number,
            address=address,
            ssp_ids=ssp_ids,
            open_date_start=open_date_start,
            open_date_end=open_date_end,
            placement_type=placement_type,
            staff_count_min=staff_count_min,
            staff_count_max=staff_count_max,
            is_active=True,
            load_org_unit=True,
        )

        workbook = Workbook()
        sheet = workbook.active
        sheet.title = "INFO"
        sheet.append(
            [
                "id",
                "ssp_id",
                "system_code",
                "regional_branch",
                "vsp_type",
                "registration_number",
                "address",
                "approved_format",
                "open_date",
                "close_date",
                "notes",
                "placement_type",
                "staff_count",
                "area",
                "rent_contract_num",
                "rent_end_date",
                "is_active",
                "is_deleted",
                "created_at",
                "updated_at",
                "created_by",
                "updated_by",
            ]
        )
        for item in data:
            sheet.append(
                [
                    item.id,
                    item.branch_id,
                    item.system_code or None,
                    item.org_unit.title or None,
                    item.vsp_type or None,
                    item.reg_number,
                    item.address,
                    item.format or None,
                    item.opened_at.isoformat() if item.opened_at else None,
                    item.closed_at.isoformat() if item.closed_at else None,
                    item.notes or None,
                    item.placement_type or None,
                    item.staff_count if item.staff_count is not None else None,
                    float(item.total_area) if item.total_area is not None else None,
                    item.rent_contract_num or None,
                    item.rent_end_date.isoformat() if item.rent_end_date else None,
                    item.is_active,
                    item.is_deleted,
                    item.created_at.isoformat() if item.created_at else None,
                    item.updated_at.isoformat() if item.updated_at else None,
                    item.created_by,
                    item.updated_by if item.updated_by is not None else None,
                ]
            )

        stream = io.BytesIO()
        workbook.save(stream)
        workbook.close()
        stream.seek(0)
        file_name = f"info_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
        return stream, file_name

    async def update(self, vsp_id: int, payload: VSPUpdate, user: AppUser, load_org_unit: bool = False) -> Vsp | None:
        vsp = await self.vsp_repo.get(vsp_id=vsp_id, is_active=None)
        if not vsp:
            return None

        data = payload.model_dump(exclude_unset=True)
        if not data:
            return vsp

        if "branch_id" in data:
            await self._ensure_active_branch(data["branch_id"])

        if user.role_id != UserRoleEnum.ADMIN:
            ssp_ids = await self._get_scoped_ssp_ids(user=user)
            if not ssp_ids or vsp.branch_id not in set(ssp_ids):
                raise AccessDeniedException()
            if not set(data.keys()).issubset(self.EXECUTOR_EDITABLE_FIELDS):
                raise AccessDeniedException()

        effective_close_date = data.get("close_date", vsp.closed_at)
        if effective_close_date is not None and effective_close_date <= date.today():
            data["is_active"] = False

        try:
            return await self.vsp_repo.update(
                vsp=vsp, data=data, updated_by=user.id, load_org_unit=load_org_unit,
            )
        except IntegrityError as exc:
            message = str(getattr(exc, "orig", exc) or exc).strip()
            if "vsp_reg_number_unique" in message:
                raise ValidationException(description="Указанный номер уже существует")
            if "chk_closed_after_opened" in message:
                raise ValidationException(description="Дата закрытия не может быть меньше даты открытия")
            if "chk_rent_date" in message:
                raise ValidationException(description="Срок окончания договора аренды не может быть меньше даты открытия")
            raise ValidationException(description="Некорректные данные для записи ВСП") from exc

    async def logical_delete(self, vsp_id: int, user: AppUser) -> bool:
        if not self._can_modify(user):
            raise AccessDeniedException()
        return await self.vsp_repo.logical_delete(vsp_id=vsp_id, updated_by=user.id)

    def _can_modify(self, user: AppUser) -> bool:
        return user.role_id == UserRoleEnum.ADMIN

    async def _get_scoped_ssp_ids(self, user: AppUser) -> list[int] | None:
        if user.role_id == UserRoleEnum.ADMIN:
            return None
        return list(set(await self.user_repo.get_many_ssp_ids(user_id=user.id)))

    async def _sync_closed_records(self) -> None:
        global _last_info_close_sync_run
        today = date.today()
        if _last_info_close_sync_run is not None and _last_info_close_sync_run >= today:
            return
        async with _info_close_sync_lock:
            if _last_info_close_sync_run is not None and _last_info_close_sync_run >= today:
                return
            await self.vsp_repo.deactivate_by_close_date(as_of=today)
            _last_info_close_sync_run = today

    async def _ensure_active_branch(self, branch_id: int | None) -> None:
        if branch_id is None:
            return
        org_unit = await self.org_unit_repo.get(org_unit_id=branch_id)
        if org_unit is None or not org_unit.is_active:
            raise ValidationException(description="Указан неактивный или отсутствующий ССП/РФ")
