from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from sqlalchemy import extract
from typing import List, Optional
from datetime import date, date as date_type, time as time_type, timedelta
from pydantic import BaseModel
import calendar
import random

from app.database import get_db
from app.models.mass_template import MassTemplate
from app.models.mass_schedule import MassSchedule
from app.models.assignment import Assignment
from app.models.asim import Asim
from app.models.template_position import TemplatePosition
from app.models.unavailability import Unavailability
from app.models.area_unavailability import AsimAreaUnavailability
from app.schemas.schedule import (
    MassScheduleResponse, AssignmentResponse,
    GenerateRequest, PublishRequest
)
from app.core.security import require_pengurus

router = APIRouter(prefix="/api/schedules", tags=["Schedules"])

# ── Helpers ────────────────────────────────────────────────

def get_dates_for_template(template: MassTemplate, year: int, month: int) -> List[date]:
    dates = []
    num_days = calendar.monthrange(year, month)[1]
    for day in range(1, num_days + 1):
        d = date(year, month, day)
        if d.weekday() == template.day_of_week:
            dates.append(d)
    return dates

def build_schedule_response(schedule: MassSchedule) -> MassScheduleResponse:
    position_order = {}
    position_area = {}  # position_number → (area_id, area_name)
    if schedule.template:
        for p in schedule.template.positions:
            position_order[p.position_number] = p.sort_order
            if p.posisi and p.posisi.area:
                position_area[p.position_number] = (p.posisi.area_id, p.posisi.area.nama)

    assignments = []
    for a in schedule.assignments:
        area_id, area_name = position_area.get(a.position_number, (None, None))
        assignments.append(AssignmentResponse(
            id=a.id,
            position_number=a.position_number,
            position_type_code=a.position_type_code,
            position_type_label=a.position_type.label if a.position_type else None,
            position_type_color=a.position_type.color if a.position_type else None,
            position_type_requires_asim=a.position_type.requires_asim if a.position_type else True,
            asim_id=a.asim_id,
            asim_name=a.asim.full_name if a.asim else None,
            asim_no=a.asim.no_asim if a.asim else None,
            is_override=a.is_override,
            area_id=area_id,
            area_name=area_name,
        ))

    assignments.sort(key=lambda x: position_order.get(x.position_number, 999))

    return MassScheduleResponse(
        id=schedule.id,
        template_id=schedule.template_id,
        date=schedule.date,
        notes=schedule.notes,
        is_published=schedule.is_published,
        template_name=schedule.template.name if schedule.template else None,
        template_time=schedule.template.time if schedule.template else None,
        assignments=assignments,
    )

def get_week_start(d: date) -> date:
    """Hitung awal minggu (Sabtu) dari tanggal tertentu."""
    # Python weekday(): Mon=0, Tue=1, Wed=2, Thu=3, Fri=4, Sat=5, Sun=6
    days_since_saturday = (d.weekday() - 5) % 7
    return d - timedelta(days=days_since_saturday)

def is_unavailable(asim_id: int, schedule_date: date, template: MassTemplate, unavailabilities: list) -> bool:
    for u in unavailabilities:
        if u.asim_id != asim_id:
            continue
        if u.unavail_type == 'date_range':
            if u.date_from and u.date_to:
                if u.date_from <= schedule_date <= u.date_to:
                    if u.mass_time is None:
                        return True
                    if template.time:
                        t_h, t_m = template.time.split(':')
                        template_time = time_type(int(t_h), int(t_m))
                        if u.mass_time == template_time:
                            return True
        elif u.unavail_type == 'recurring':
            if u.day_of_week is not None and u.day_of_week != schedule_date.weekday():
                continue
            if u.mass_time is None:
                return True
            if template.time:
                t_h, t_m = template.time.split(':')
                template_time = time_type(int(t_h), int(t_m))
                if u.mass_time == template_time:
                    return True
    return False

def auto_assign(
    db: Session,
    schedule: MassSchedule,
    template: MassTemplate,
    recent_asim_dates: dict,
    assignment_counts: dict,
    unavailabilities: list = [],
    area_unavail_map: dict = {},
):
    all_asim = db.query(Asim).filter(Asim.is_active == True).all()

    # Build eligibility map
    eligible_map = {}
    for asim in all_asim:
        if hasattr(asim, 'eligibilities') and isinstance(asim.eligibilities, str):
            eligible_map[asim.id] = [e.strip() for e in asim.eligibilities.split(',') if e.strip()]
        elif hasattr(asim, 'eligibilities') and hasattr(asim.eligibilities, '__iter__'):
            eligible_map[asim.id] = [e.position_type_code for e in asim.eligibilities]
        else:
            eligible_map[asim.id] = []

    positions = sorted(template.positions, key=lambda p: p.sort_order)
    assigned_this_schedule = set()

    for pos in positions:
        pt_code = pos.position_type_code

        # Area dari posisi (jika posisi terhubung ke position_slot)
        pos_area_id = pos.posisi.area_id if pos.posisi_id and pos.posisi else None

        # Posisi yang tidak perlu ASIM (SR, kosong, dll)
        if pos.position_type and not pos.position_type.requires_asim:
            db.add(Assignment(
                schedule_id=schedule.id,
                asim_id=None,
                position_number=pos.position_number,
                position_type_code=pt_code,
                is_override=False,
            ))
            continue

        schedule_week_start = get_week_start(schedule.date)

        def get_candidates(relax_consecutive=False, _area_id=pos_area_id):
            result = []
            for asim in all_asim:
                if asim.id in assigned_this_schedule:
                    continue
                if is_unavailable(asim.id, schedule.date, template, unavailabilities):
                    continue
                if pos.position_type and pos.position_type.requires_eligibility:
                    if pt_code not in eligible_map.get(asim.id, []):
                        continue
                # Skip jika ASIM tidak bisa melayani di area ini
                if _area_id and _area_id in area_unavail_map.get(asim.id, set()):
                    continue
                last_date = recent_asim_dates.get(asim.id)
                if last_date:
                    # HARD RULE (tidak bisa di-relax): ASIM tidak boleh bertugas
                    # dua kali dalam satu week yang sama (Sabtu–Jumat)
                    if get_week_start(last_date) == schedule_week_start:
                        continue
                    # SOFT RULE (bisa di-relax): tidak boleh di minggu berturut-turut
                    if not relax_consecutive:
                        diff = abs((schedule.date - last_date).days)
                        if diff <= 7:
                            continue
                result.append(asim)
            return result

        strict_candidates = get_candidates(relax_consecutive=False)
        candidates = strict_candidates if strict_candidates else get_candidates(relax_consecutive=True)

        if not candidates:
            db.add(Assignment(
                schedule_id=schedule.id,
                asim_id=None,
                position_number=pos.position_number,
                position_type_code=pt_code,
                is_override=False,
            ))
            continue

        # Sort: fewest assignments first, then longest rested (most days since last assignment)
        def days_idle(a):
            return (schedule.date - recent_asim_dates.get(a.id, date(2000, 1, 1))).days

        candidates.sort(key=lambda a: (assignment_counts.get(a.id, 0), -days_idle(a)))
        min_count = assignment_counts.get(candidates[0].id, 0)
        top_candidates = [a for a in candidates if assignment_counts.get(a.id, 0) == min_count]
        max_idle = max(days_idle(a) for a in top_candidates)
        top_candidates = [a for a in top_candidates if days_idle(a) == max_idle]
        chosen = random.choice(top_candidates)

        db.add(Assignment(
            schedule_id=schedule.id,
            asim_id=chosen.id,
            position_number=pos.position_number,
            position_type_code=pt_code,
            is_override=False,
        ))

        assigned_this_schedule.add(chosen.id)
        assignment_counts[chosen.id] = assignment_counts.get(chosen.id, 0) + 1
        recent_asim_dates[chosen.id] = schedule.date

# ── Routes ─────────────────────────────────────────────────

@router.get("/weekly", response_model=List[MassScheduleResponse])
def get_weekly_schedules(
    date_from: Optional[str] = None,
    date_to: Optional[str] = None,
    month: Optional[int] = None,
    year: Optional[int] = None,
    db: Session = Depends(get_db)
):
    q = db.query(MassSchedule).filter(MassSchedule.template_id != None)

    if date_from and date_to:
        q = q.filter(
            MassSchedule.date >= date_type.fromisoformat(date_from),
            MassSchedule.date <= date_type.fromisoformat(date_to),
        )
    elif month and year:
        q = q.filter(
            extract('month', MassSchedule.date) == month,
            extract('year', MassSchedule.date) == year,
        )

    schedules = q.order_by(MassSchedule.date).all()

    def sort_key(s):
        time_str = s.template.time if s.template and s.template.time else "00:00"
        return (s.date, time_str)

    schedules.sort(key=sort_key)
    return [build_schedule_response(s) for s in schedules]

@router.post("/generate")
def generate_schedules(data: GenerateRequest, db: Session = Depends(get_db), _: object = Depends(require_pengurus)):
    # Hapus schedule draft bulan ini
    existing = db.query(MassSchedule).filter(
        extract('month', MassSchedule.date) == data.month,
        extract('year', MassSchedule.date) == data.year,
        MassSchedule.is_published == False,
        MassSchedule.template_id != None,
    ).all()
    for s in existing:
        db.query(Assignment).filter(Assignment.schedule_id == s.id).delete()
        db.delete(s)
    db.flush()

    # Get semua template weekly aktif
    templates = db.query(MassTemplate).filter(
        MassTemplate.mass_type == 'weekly',
        MassTemplate.is_active == True,
    ).all()

    # Load unavailabilities approved
    unavailabilities = db.query(Unavailability).filter(
        Unavailability.status == 'approved'
    ).all()

    # Load area unavailabilities aktif
    area_unavails = db.query(AsimAreaUnavailability).filter(
        AsimAreaUnavailability.is_active == True,
        AsimAreaUnavailability.status == 'approved',
    ).all()
    area_unavail_map = {}
    for au in area_unavails:
        area_unavail_map.setdefault(au.asim_id, set()).add(au.area_id)

    # Tracking assignment bulan sebelumnya untuk consecutive check
    recent_asim_dates = {}
    assignment_counts = {}

    prev_month = data.month - 1 if data.month > 1 else 12
    prev_year = data.year if data.month > 1 else data.year - 1
    prev_assignments = db.query(Assignment).join(MassSchedule).filter(
        extract('month', MassSchedule.date) == prev_month,
        extract('year', MassSchedule.date) == prev_year,
        Assignment.asim_id != None,
    ).all()
    for a in prev_assignments:
        if a.asim_id not in recent_asim_dates or \
           a.schedule.date > recent_asim_dates[a.asim_id]:
            recent_asim_dates[a.asim_id] = a.schedule.date

    # Generate semua jadwal sorted by date + time
    all_schedules = []
    schedule_queue = []
    for template in templates:
        dates = get_dates_for_template(template, data.year, data.month)
        for d in sorted(dates):
            schedule_queue.append((d, template))

    schedule_queue.sort(key=lambda x: (x[0], x[1].time or "00:00"))

    for d, template in schedule_queue:
        schedule = MassSchedule(
            template_id=template.id,
            date=d,
            is_published=False,
        )
        db.add(schedule)
        db.flush()
        auto_assign(db, schedule, template, recent_asim_dates, assignment_counts, unavailabilities, area_unavail_map)
        all_schedules.append(schedule)

    try:
        db.commit()
    except Exception:
        db.rollback()
        raise HTTPException(status_code=500, detail="Gagal menyimpan jadwal yang di-generate")
    for s in all_schedules:
        db.refresh(s)

    return {"message": f"Generated {len(all_schedules)} jadwal", "count": len(all_schedules)}

class OverrideRequest(BaseModel):
    asim_id: Optional[int] = None
    position_type_code: Optional[str] = None

@router.put("/assignments/{assignment_id}/override")
def override_assignment(assignment_id: int, data: OverrideRequest, db: Session = Depends(get_db), _: object = Depends(require_pengurus)):
    a = db.query(Assignment).filter(Assignment.id == assignment_id).first()
    if not a:
        raise HTTPException(status_code=404, detail="Assignment tidak ditemukan")

    cleared_assignment_ids = []

    if 'asim_id' in data.model_fields_set and data.asim_id is not None:
        new_asim_id = data.asim_id
        # Cek apakah ASIM ini sudah bertugas di minggu yang sama (Sabtu–Jumat)
        target_schedule = db.query(MassSchedule).filter(MassSchedule.id == a.schedule_id).first()
        if target_schedule:
            week_start = get_week_start(target_schedule.date)
            week_end = week_start + timedelta(days=6)
            # Cari semua assignment ASIM ini di minggu yang sama (kecuali assignment ini sendiri)
            same_week = db.query(Assignment).join(MassSchedule).filter(
                Assignment.asim_id == new_asim_id,
                Assignment.id != assignment_id,
                MassSchedule.date >= week_start,
                MassSchedule.date <= week_end,
            ).all()
            for conflict in same_week:
                cleared_assignment_ids.append(conflict.id)
                conflict.asim_id = None
                conflict.is_override = True
        a.asim_id = new_asim_id

    elif 'asim_id' in data.model_fields_set and data.asim_id is None:
        a.asim_id = None

    if 'position_type_code' in data.model_fields_set and data.position_type_code is not None:
        a.position_type_code = data.position_type_code
        a.asim_id = None  # reset asim saat tipe berubah

    a.is_override = True
    try:
        db.commit()
    except Exception:
        db.rollback()
        raise HTTPException(status_code=500, detail="Gagal menyimpan override")
    return {"message": "Override berhasil", "cleared_assignment_ids": cleared_assignment_ids}

@router.post("/publish")
def publish_schedules(data: PublishRequest, db: Session = Depends(get_db), _: object = Depends(require_pengurus)):
    for schedule_id in data.schedule_ids:
        schedule = db.query(MassSchedule).filter(MassSchedule.id == schedule_id).first()
        if not schedule:
            continue

        existing_published = db.query(MassSchedule).filter(
            MassSchedule.template_id == schedule.template_id,
            MassSchedule.date == schedule.date,
            MassSchedule.is_published == True,
            MassSchedule.id != schedule_id,
        ).first()

        if existing_published:
            db.query(Assignment).filter(Assignment.schedule_id == existing_published.id).delete()
            db.delete(existing_published)

        schedule.is_published = True

    try:
        db.commit()
    except Exception:
        db.rollback()
        raise HTTPException(status_code=500, detail="Gagal publish jadwal")
    return {"message": f"Published {len(data.schedule_ids)} jadwal"}


class ResetDraftRequest(BaseModel):
    month: int
    year: int

@router.delete("/draft")
def reset_draft(data: ResetDraftRequest, db: Session = Depends(get_db), _: object = Depends(require_pengurus)):
    """Hapus semua jadwal draft (unpublished) pada bulan tertentu."""
    drafts = db.query(MassSchedule).filter(
        extract('month', MassSchedule.date) == data.month,
        extract('year', MassSchedule.date) == data.year,
        MassSchedule.is_published == False,
        MassSchedule.template_id != None,
    ).all()
    count = len(drafts)
    for s in drafts:
        db.query(Assignment).filter(Assignment.schedule_id == s.id).delete()
        db.delete(s)
    try:
        db.commit()
    except Exception:
        db.rollback()
        raise HTTPException(status_code=500, detail="Gagal reset draft")
    return {"message": f"Reset {count} draft jadwal"}