from decimal import Decimal, ROUND_HALF_UP

from django.db import transaction
from django.utils import timezone

from apps.accounts.models import Role
from apps.distributions.models import (
    ApprovalDecision,
    ApprovalRecord,
    ApprovalStep,
    ApprovalWorkflow,
    DistributionItem,
    ProfitCalculation,
    ProfitDistribution,
    WorkflowStatus,
)
from apps.investments.models import InvestmentStatus
from apps.notifications.models import NotificationKind
from apps.notifications.services import notify_user
from apps.statements.models import Statement, StatementPeriod
from apps.transactions.models import Transaction, TransactionStatus, TransactionType


STEP_ORDER = [
    ApprovalStep.MAKER,
    ApprovalStep.CHECKER,
    ApprovalStep.FINANCE,
    ApprovalStep.DIRECTOR,
    ApprovalStep.CEO,
]

STEP_ROLES = {
    ApprovalStep.MAKER: {Role.FINANCE_MANAGER, Role.PROJECT_MANAGER, Role.ADMIN, Role.SUPER_ADMIN},
    ApprovalStep.CHECKER: {Role.FINANCE_MANAGER, Role.ADMIN, Role.SUPER_ADMIN},
    ApprovalStep.FINANCE: {Role.FINANCE_MANAGER, Role.ADMIN, Role.SUPER_ADMIN},
    ApprovalStep.DIRECTOR: {Role.ADMIN, Role.SUPER_ADMIN},
    ApprovalStep.CEO: {Role.SUPER_ADMIN, Role.ADMIN},
}


def start_workflow(module, obj, user):
    workflow = ApprovalWorkflow.objects.create(
        module=module,
        object_type=obj.__class__.__name__,
        object_id=obj.pk,
        current_step=ApprovalStep.MAKER,
        status=WorkflowStatus.IN_REVIEW,
        submitted_by=user,
    )
    ApprovalRecord.objects.create(workflow=workflow, step=ApprovalStep.MAKER, decision=ApprovalDecision.PENDING)
    return workflow


@transaction.atomic
def advance_workflow(workflow, user, decision, notes=""):
    if workflow.status not in {WorkflowStatus.IN_REVIEW, WorkflowStatus.DRAFT}:
        raise ValueError("This workflow is no longer open for review.")
    allowed = STEP_ROLES.get(workflow.current_step, set())
    if user.role not in allowed:
        raise ValueError(f"Your role cannot act on the {workflow.current_step} step.")
    record = workflow.records.filter(step=workflow.current_step, decision=ApprovalDecision.PENDING).first()
    if record is None:
        record = ApprovalRecord.objects.create(workflow=workflow, step=workflow.current_step)
    record.decision = decision
    record.actor = user
    record.notes = notes
    record.decided_at = timezone.now()
    record.save()
    if decision == ApprovalDecision.REJECTED:
        workflow.status = WorkflowStatus.REJECTED
        workflow.save(update_fields=["status", "updated_at"])
        return workflow
    idx = STEP_ORDER.index(workflow.current_step)
    if idx >= len(STEP_ORDER) - 1:
        workflow.status = WorkflowStatus.APPROVED
        workflow.save(update_fields=["status", "updated_at"])
        return workflow
    workflow.current_step = STEP_ORDER[idx + 1]
    workflow.status = WorkflowStatus.IN_REVIEW
    workflow.save(update_fields=["current_step", "status", "updated_at"])
    ApprovalRecord.objects.create(workflow=workflow, step=workflow.current_step, decision=ApprovalDecision.PENDING)
    return workflow


@transaction.atomic
def create_profit_calculation(project, user, sales_revenue, eligible_costs, notes=""):
    sales_revenue = Decimal(sales_revenue)
    eligible_costs = Decimal(eligible_costs)
    profit = (sales_revenue - eligible_costs).quantize(Decimal("0.01"))
    calc = ProfitCalculation.objects.create(
        project=project,
        sales_revenue=sales_revenue,
        eligible_costs=eligible_costs,
        distributable_profit=profit,
        notes=notes,
        created_by=user,
        status=WorkflowStatus.DRAFT,
    )
    return calc


@transaction.atomic
def submit_profit_calculation(calc, user):
    if calc.status not in {WorkflowStatus.DRAFT, WorkflowStatus.REJECTED}:
        raise ValueError("Only draft or rejected calculations can be submitted.")
    calc.workflow = start_workflow("distributions", calc, user)
    calc.status = WorkflowStatus.IN_REVIEW
    calc.save(update_fields=["workflow", "status", "updated_at"])
    return calc


@transaction.atomic
def approve_profit_calculation(calc, user, decision, notes=""):
    if calc.workflow is None:
        raise ValueError("This calculation has not been submitted.")
    workflow = advance_workflow(calc.workflow, user, decision, notes)
    if workflow.status == WorkflowStatus.REJECTED:
        calc.status = WorkflowStatus.REJECTED
        calc.save(update_fields=["status", "updated_at"])
        return calc
    if workflow.status == WorkflowStatus.APPROVED:
        materialize_distribution(calc, user)
        calc.status = WorkflowStatus.APPROVED
        calc.save(update_fields=["status", "updated_at"])
    return calc


@transaction.atomic
def materialize_distribution(calc, user):
    distribution, _ = ProfitDistribution.objects.get_or_create(
        calculation=calc,
        defaults={"project": calc.project, "status": WorkflowStatus.APPROVED},
    )
    distribution.status = WorkflowStatus.APPROVED
    distribution.save()
    distribution.items.all().delete()
    investments = calc.project.investments.filter(status=InvestmentStatus.ACTIVE)
    for investment in investments:
        share = investment.profit_share_percent or investment.participation_percent
        amount = (calc.distributable_profit * (share / Decimal("100"))).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
        item = DistributionItem.objects.create(
            distribution=distribution,
            investor=investment.investor,
            investment=investment,
            participation_percent=share,
            amount=amount,
            status=DistributionItem.ItemStatus.APPROVED,
        )
        notify_user(
            investment.investor.user,
            "Distribution calculated",
            (
                f"{calc.project.name}: a distributable allocation of ৳ {amount} "
                f"({share}%) is pending payment execution."
            ),
            kind=NotificationKind.TRANSACTION,
            project=calc.project,
        )
        _ = item
    return distribution


@transaction.atomic
def execute_distribution(calc, user):
    if calc.status != WorkflowStatus.APPROVED:
        raise ValueError("Calculation must be fully approved before payment.")
    distribution = getattr(calc, "distribution", None)
    if distribution is None:
        distribution = materialize_distribution(calc, user)
    if distribution.status == WorkflowStatus.EXECUTED:
        return distribution
    year = timezone.now().year
    for item in distribution.items.select_related("investor", "investment", "investment__project"):
        count = Transaction.objects.count() + 1
        Transaction.objects.create(
            reference=f"TX-BEN-{year}-{count:04d}",
            investor=item.investor,
            investment=item.investment,
            project=calc.project,
            txn_type=TransactionType.BENEFIT,
            status=TransactionStatus.POSTED,
            amount=item.amount,
            occurred_on=timezone.now().date(),
            description=f"Approved distribution for {calc.project.name}",
            posted_by=user,
        )
        item.investment.benefit_received = (item.investment.benefit_received or 0) + item.amount
        item.investment.save(update_fields=["benefit_received", "updated_at"])
        item.status = DistributionItem.ItemStatus.PAID
        item.save(update_fields=["status", "updated_at"])
        Statement.objects.create(
            investor=item.investor,
            investment=item.investment,
            title=f"Distribution statement · {calc.project.name}",
            period=StatementPeriod.MONTHLY,
            period_label=timezone.now().strftime("%B %Y"),
            summary=(
                f"Approved distribution of ৳ {item.amount} against participation "
                f"{item.participation_percent}%."
            ),
            posted_on=timezone.now().date(),
        )
        notify_user(
            item.investor.user,
            "Distribution paid",
            f"৳ {item.amount} has been posted to your ledger for {calc.project.name}.",
            kind=NotificationKind.TRANSACTION,
            project=calc.project,
        )
    distribution.status = WorkflowStatus.EXECUTED
    distribution.executed_at = timezone.now()
    distribution.executed_by = user
    distribution.save()
    calc.status = WorkflowStatus.EXECUTED
    calc.save(update_fields=["status", "updated_at"])
    return distribution
