import json
import os
from datetime import date, datetime, timedelta, timezone
from typing import Dict, List, Optional

from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
from sqlalchemy.orm import Session

from app.core.database import get_db
from app.helpers.crud import get_or_404
from app.models.investment import Investment, InvestmentAttachment
from app.schemas.portfolio import (
    AttachmentOut,
    DashboardOut,
    InvestmentCreate,
    InvestmentOut,
    InvestmentUpdate,
    PortfolioConfigOut,
    UpcomingEvent,
)

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

# ── Config ────────────────────────────────────────────────────────────────────

_CONFIG_PATH = os.path.join(
    os.path.dirname(__file__), "..", "config", "portfolio_config.json"
)


def _load_config() -> dict:
    with open(_CONFIG_PATH, "r") as f:
        return json.load(f)


@router.get("/config", response_model=PortfolioConfigOut)
def get_portfolio_config():
    return _load_config()


# ── Calculation helpers ────────────────────────────────────────────────────────

_PERIODS_PER_YEAR = {
    "monthly": 12,
    "quarterly": 4,
    "semi_annual": 2,
    "annual": 1,
    "at_maturity": 1,
    "none": 0,
}

_FREQ_DELTA = {
    "monthly": timedelta(days=30),
    "quarterly": timedelta(days=91),
    "semi_annual": timedelta(days=182),
    "annual": timedelta(days=365),
}


def _calculated_fields(inv: Investment) -> dict:
    """Return dict of runtime-calculated fields for an Investment ORM object."""
    total_face_value = inv.units * inv.face_value_per_unit
    total_cost_idr = int(
        total_face_value * (inv.purchase_price / 100.0) * inv.kurs_beli
    )

    periods = _PERIODS_PER_YEAR.get(inv.coupon_frequency, 0)
    if periods > 0:
        gross_per_period = int(
            total_face_value * (inv.interest_rate / 100.0) / periods
        )
        net_per_period = int(gross_per_period * (1.0 - inv.tax_rate / 100.0))
        annual_net = net_per_period * periods
    else:
        gross_per_period = 0
        net_per_period = 0
        annual_net = 0

    return {
        "total_face_value": total_face_value,
        "total_cost_idr": total_cost_idr,
        "gross_coupon_per_period": gross_per_period,
        "net_coupon_per_period": net_per_period,
        "annual_net_income": annual_net,
    }


def _as_date(value) -> Optional[date]:
    """Normalize a Date/DateTime ORM value to a plain date."""
    if value is None:
        return None
    if isinstance(value, datetime):
        return value.date()
    return value


def _to_investment_out(
    inv: Investment, include_attachments: bool = False
) -> InvestmentOut:
    calc = _calculated_fields(inv)
    attachments = (
        [AttachmentOut.model_validate(a) for a in inv.attachments]
        if include_attachments
        else None
    )
    return InvestmentOut(
        id=inv.id,
        bank=inv.bank,
        platform=inv.platform,
        investment_number=inv.investment_number,
        investment_type=inv.investment_type,
        investment_name=inv.investment_name,
        currency=inv.currency,
        kurs_beli=inv.kurs_beli,
        owner_name=inv.owner_name,
        investment_date=_as_date(inv.investment_date),
        tenor_months=inv.tenor_months,
        maturity_date=_as_date(inv.maturity_date),
        units=inv.units,
        face_value_per_unit=inv.face_value_per_unit,
        purchase_price=inv.purchase_price,
        nominal_idr=inv.nominal_idr,
        interest_rate=inv.interest_rate,
        coupon_frequency=inv.coupon_frequency,
        next_coupon_date=_as_date(inv.next_coupon_date),
        tax_rate=inv.tax_rate,
        is_tax_inclusive=inv.is_tax_inclusive,
        status=inv.status,
        sold_date=_as_date(inv.sold_date),
        sold_price=inv.sold_price,
        notes=inv.notes,
        created_at=inv.created_at,
        updated_at=inv.updated_at,
        attachments=attachments,
        **calc,
    )


# ── Investments CRUD ───────────────────────────────────────────────────────────

@router.get("/investments", response_model=List[InvestmentOut])
def list_investments(db: Session = Depends(get_db)):
    investments = (
        db.query(Investment)
        .order_by(Investment.investment_date.desc())
        .all()
    )
    return [_to_investment_out(inv) for inv in investments]


@router.post(
    "/investments",
    response_model=InvestmentOut,
    status_code=status.HTTP_201_CREATED,
)
def create_investment(body: InvestmentCreate, db: Session = Depends(get_db)):
    inv = Investment(**body.model_dump())
    db.add(inv)
    db.commit()
    db.refresh(inv)
    return _to_investment_out(inv)


@router.get("/investments/{investment_id}", response_model=InvestmentOut)
def get_investment(investment_id: str, db: Session = Depends(get_db)):
    inv = get_or_404(db, Investment, investment_id, "Investment")
    return _to_investment_out(inv, include_attachments=True)


@router.put("/investments/{investment_id}", response_model=InvestmentOut)
def update_investment(
    investment_id: str,
    body: InvestmentUpdate,
    db: Session = Depends(get_db),
):
    inv = get_or_404(db, Investment, investment_id, "Investment")
    for field, value in body.model_dump(exclude_none=True).items():
        setattr(inv, field, value)
    inv.updated_at = datetime.now(timezone.utc)
    db.commit()
    db.refresh(inv)
    return _to_investment_out(inv)


@router.delete("/investments/{investment_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_investment(investment_id: str, db: Session = Depends(get_db)):
    inv = get_or_404(db, Investment, investment_id, "Investment")
    # Best-effort: delete stored files before removing DB record
    from app.helpers.file_storage import delete_file  # noqa: PLC0415

    for att in list(inv.attachments):
        try:
            delete_file(att.google_drive_file_id)  # stores the local filename
        except Exception:
            pass
    db.delete(inv)
    db.commit()


# ── Dashboard ──────────────────────────────────────────────────────────────────

@router.get("/dashboard", response_model=DashboardOut)
def get_dashboard(db: Session = Depends(get_db)):
    active = db.query(Investment).filter(Investment.status == "active").all()

    today = date.today()
    horizon = today + timedelta(days=90)

    total_value_idr = 0
    total_cost_idr = 0
    annual_net_income = 0
    upcoming: List[UpcomingEvent] = []
    allocation_by_type: Dict[str, int] = {}
    allocation_by_currency: Dict[str, int] = {}
    allocation_by_bank: Dict[str, int] = {}

    for inv in active:
        calc = _calculated_fields(inv)

        total_value_idr += inv.nominal_idr
        total_cost_idr += calc["total_cost_idr"]
        annual_net_income += calc["annual_net_income"]

        allocation_by_type[inv.investment_type] = (
            allocation_by_type.get(inv.investment_type, 0) + inv.nominal_idr
        )
        allocation_by_currency[inv.currency] = (
            allocation_by_currency.get(inv.currency, 0) + inv.nominal_idr
        )
        allocation_by_bank[inv.bank] = (
            allocation_by_bank.get(inv.bank, 0) + inv.nominal_idr
        )

        # ── Coupon events (periodic frequencies) ──────────────────────────────
        freq = inv.coupon_frequency
        if (
            freq not in ("none", "at_maturity", None)
            and freq in _FREQ_DELTA
            and inv.next_coupon_date is not None
        ):
            event_date = _as_date(inv.next_coupon_date)
            delta = _FREQ_DELTA[freq]
            # Advance to the next upcoming date
            while event_date < today:
                event_date = event_date + delta
            if event_date <= horizon:
                upcoming.append(
                    UpcomingEvent(
                        type="coupon",
                        investment_id=inv.id,
                        investment_name=inv.investment_name,
                        investment_type=inv.investment_type,
                        date=event_date,
                        days_until=(event_date - today).days,
                        net_amount=calc["net_coupon_per_period"],
                        currency=inv.currency,
                    )
                )

        # ── At-maturity coupon event ───────────────────────────────────────────
        if freq == "at_maturity" and inv.maturity_date is not None:
            mat_date = _as_date(inv.maturity_date)
            if today <= mat_date <= horizon:
                upcoming.append(
                    UpcomingEvent(
                        type="coupon",
                        investment_id=inv.id,
                        investment_name=inv.investment_name,
                        investment_type=inv.investment_type,
                        date=mat_date,
                        days_until=(mat_date - today).days,
                        net_amount=calc["net_coupon_per_period"],
                        currency=inv.currency,
                    )
                )

        # ── Maturity event ─────────────────────────────────────────────────────
        if inv.maturity_date is not None:
            mat_date = _as_date(inv.maturity_date)
            if today <= mat_date <= horizon:
                upcoming.append(
                    UpcomingEvent(
                        type="maturity",
                        investment_id=inv.id,
                        investment_name=inv.investment_name,
                        investment_type=inv.investment_type,
                        date=mat_date,
                        days_until=(mat_date - today).days,
                        nominal_idr=inv.nominal_idr,
                        currency=inv.currency,
                    )
                )

    upcoming.sort(key=lambda e: e.date)

    return DashboardOut(
        total_value_idr=total_value_idr,
        total_cost_idr=total_cost_idr,
        unrealized_pnl=total_value_idr - total_cost_idr,
        annual_net_income=annual_net_income,
        investment_count=len(active),
        upcoming_events=upcoming,
        allocation_by_type=allocation_by_type,
        allocation_by_currency=allocation_by_currency,
        allocation_by_bank=allocation_by_bank,
    )


# ── Attachments ────────────────────────────────────────────────────────────────

@router.get(
    "/investments/{investment_id}/attachments",
    response_model=List[AttachmentOut],
)
def list_attachments(investment_id: str, db: Session = Depends(get_db)):
    get_or_404(db, Investment, investment_id, "Investment")
    return (
        db.query(InvestmentAttachment)
        .filter(InvestmentAttachment.investment_id == investment_id)
        .order_by(InvestmentAttachment.created_at)
        .all()
    )


@router.post(
    "/investments/{investment_id}/attachments",
    response_model=AttachmentOut,
    status_code=status.HTTP_201_CREATED,
)
async def upload_attachment(
    investment_id: str,
    file: UploadFile = File(...),
    db: Session = Depends(get_db),
):
    get_or_404(db, Investment, investment_id, "Investment")

    file_data = await file.read()

    try:
        from app.helpers.file_storage import save_file  # noqa: PLC0415

        stored_filename, view_url = save_file(
            file_data=file_data,
            original_filename=file.filename or "attachment",
        )
    except Exception as exc:
        raise HTTPException(
            status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
            detail=f"File upload failed: {exc}",
        )

    att = InvestmentAttachment(
        investment_id=investment_id,
        google_drive_file_id=stored_filename,  # stores local filename (UUID-based)
        google_drive_view_url=view_url,         # stores /api/portfolio/attachments/<filename>
        original_filename=file.filename or "attachment",
        content_type=file.content_type or "application/octet-stream",
        file_size=len(file_data),
    )
    db.add(att)
    db.commit()
    db.refresh(att)
    return att


@router.delete(
    "/investments/{investment_id}/attachments/{attachment_id}",
    status_code=status.HTTP_204_NO_CONTENT,
)
def delete_attachment(
    investment_id: str,
    attachment_id: str,
    db: Session = Depends(get_db),
):
    get_or_404(db, Investment, investment_id, "Investment")
    att = db.get(InvestmentAttachment, attachment_id)
    if not att or att.investment_id != investment_id:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Attachment not found",
        )

    # Best-effort local file deletion
    try:
        from app.helpers.file_storage import delete_file  # noqa: PLC0415

        delete_file(att.google_drive_file_id)  # stores the local filename
    except Exception:
        pass

    db.delete(att)
    db.commit()
