import csv
import io
import os
import uuid
from datetime import date, datetime
from functools import wraps

from flask import (
    Blueprint,
    abort,
    flash,
    make_response,
    redirect,
    render_template,
    request,
    send_from_directory,
    url_for,
)
from flask_login import current_user, login_required
from reportlab.lib import colors
from reportlab.lib.enums import TA_CENTER
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import mm
from reportlab.platypus import PageBreak, Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle
from sqlalchemy import func, or_
from werkzeug.utils import secure_filename

from .extensions import db
from .models import (
    ActionItem,
    Attachment,
    Branch,
    Comment,
    Department,
    Incident,
    Notification,
    RCA,
    User,
)
from .policy import (
    CLASSIFICATIONS,
    CONTRIBUTORY_FACTORS,
    HARM_LEVELS,
    INJURY_TYPES,
    LIKELIHOODS,
    MEDICATION_HARM,
    PERSON_TYPES,
    REPORT_TYPES,
    RISK_COLORS,
    SENTINEL_EVENTS,
    add_working_days,
    action_plan_due,
    due_date_for_risk,
    risk_level,
    risk_score,
    utcnow,
)
from .services import (
    assign_reference,
    audit,
    can_view_incident,
    find_manager,
    find_qps,
    incident_query_for,
    notify,
    notify_role,
    reporter_display,
    transition,
    upload_root,
)


bp = Blueprint("main", __name__)
ALLOWED_EXTENSIONS = {"pdf", "png", "jpg", "jpeg", "doc", "docx", "xls", "xlsx"}


def permission_required(code):
    def decorator(view):
        @wraps(view)
        @login_required
        def wrapped(*args, **kwargs):
            if not current_user.can(code):
                abort(403)
            return view(*args, **kwargs)

        return wrapped

    return decorator


def visible_incident(incident_id):
    incident = db.get_or_404(Incident, incident_id)
    if not can_view_incident(current_user, incident):
        abort(403)
    return incident


def to_bool(value):
    if value in (None, ""):
        return None
    return str(value).lower() in {"1", "true", "yes", "on"}


def parse_date(value):
    try:
        return date.fromisoformat(value)
    except (TypeError, ValueError):
        return None


def parse_time(value):
    try:
        return datetime.strptime(value, "%H:%M").time()
    except (TypeError, ValueError):
        return None


def form_options():
    branches = db.session.scalars(db.select(Branch).where(Branch.is_active.is_(True)).order_by(Branch.name)).all()
    departments = db.session.scalars(
        db.select(Department).where(Department.is_active.is_(True)).order_by(Department.branch_id, Department.name)
    ).all()
    return {
        "today": date.today().isoformat(),
        "branches": branches,
        "departments": departments,
        "classifications": CLASSIFICATIONS,
        "person_types": PERSON_TYPES,
        "report_types": REPORT_TYPES,
        "injury_types": INJURY_TYPES,
        "harm_levels": HARM_LEVELS,
        "likelihoods": LIKELIHOODS,
        "medication_harm": MEDICATION_HARM,
    }


def populate_incident(incident):
    branch_id = request.form.get("branch_id", type=int) or current_user.branch_id
    department_id = request.form.get("department_id", type=int) or current_user.department_id
    branch = db.session.get(Branch, branch_id)
    department = db.session.get(Department, department_id)
    if not branch or not branch.is_active or not department or department.branch_id != branch.id or not department.is_active:
        raise ValueError("Select a valid active branch and department.")
    if current_user.role.scope != "all" and current_user.branch_id and branch.id != current_user.branch_id:
        raise ValueError("You cannot report for another branch.")

    category = request.form.get("classification_category", "").strip()
    if category not in CLASSIFICATIONS:
        raise ValueError("Select a valid occurrence classification.")
    selected_items = [item for item in request.form.getlist("classification_items") if item in CLASSIFICATIONS[category]]
    if not selected_items:
        raise ValueError("Select at least one classification item.")
    report_type = request.form.get("report_type", "").strip()
    if report_type not in REPORT_TYPES:
        raise ValueError("Select what is being reported.")
    person_type = request.form.get("person_type", "").strip()
    if person_type not in PERSON_TYPES:
        raise ValueError("Select the person involved.")
    incident_date = parse_date(request.form.get("incident_date"))
    incident_time = parse_time(request.form.get("incident_time"))
    if not incident_date or not incident_time or incident_date > date.today():
        raise ValueError("Enter a valid incident date and time that is not in the future.")

    description = request.form.get("description", "").strip()
    location = request.form.get("location", "").strip()
    if len(description) < 20:
        raise ValueError("Provide a factual description of at least 20 characters.")
    if not location:
        raise ValueError("Incident location is required.")

    incident.branch = branch
    incident.department = department
    incident.is_anonymous = bool(request.form.get("is_anonymous"))
    incident.incident_date = incident_date
    incident.incident_time = incident_time
    incident.location = location
    incident.person_type = person_type
    incident.person_name = request.form.get("person_name", "").strip() or None
    incident.person_position = request.form.get("person_position", "").strip() or None
    incident.person_mobile_badge = request.form.get("person_mobile_badge", "").strip() or None
    incident.person_department = request.form.get("person_department", "").strip() or None
    incident.patient_mrn = request.form.get("patient_mrn", "").strip() or None
    incident.classification_category = category
    incident.classification_items = selected_items
    incident.classification_other = request.form.get("classification_other", "").strip() or None
    incident.report_type = report_type
    incident.injury_occurred = bool(request.form.get("injury_occurred"))
    incident.injury_type = request.form.get("injury_type") if request.form.get("injury_type") in INJURY_TYPES else None
    incident.harm_level = request.form.get("harm_level") if request.form.get("harm_level") in HARM_LEVELS else None
    incident.likelihood = request.form.get("likelihood") if request.form.get("likelihood") in LIKELIHOODS else None
    medication = request.form.get("medication_harm_category", "")
    incident.medication_harm_category = medication if medication in MEDICATION_HARM else None
    incident.description = description
    incident.immediate_action = request.form.get("immediate_action", "").strip() or None

    involved_ids = request.form.getlist("involved_department_ids", type=int)
    incident.involved_departments = db.session.scalars(
        db.select(Department).where(
            Department.id.in_(involved_ids or [-1]), Department.branch_id == branch.id, Department.is_active.is_(True)
        )
    ).all()


@bp.get("/healthz")
def healthz():
    try:
        db.session.execute(db.select(1))
        return {"status": "ok"}
    except Exception:
        return {"status": "unavailable"}, 503


@bp.get("/")
def index():
    return redirect(url_for("main.dashboard") if current_user.is_authenticated else url_for("auth.login"))


@bp.get("/dashboard")
@login_required
def dashboard():
    query = incident_query_for(current_user)
    incidents = db.session.scalars(query.order_by(Incident.updated_at.desc()).limit(8)).all()
    all_visible = query.subquery()
    counts = {
        "all": db.session.scalar(db.select(func.count()).select_from(all_visible)) or 0,
        "open": db.session.scalar(db.select(func.count()).select_from(all_visible).where(all_visible.c.status != "CLOSED")) or 0,
        "high": db.session.scalar(
            db.select(func.count()).select_from(all_visible).where(all_visible.c.risk_level.in_(["High", "Extreme"]))
        ) or 0,
        "closed": db.session.scalar(db.select(func.count()).select_from(all_visible).where(all_visible.c.status == "CLOSED")) or 0,
    }
    now = utcnow()
    overdue = db.session.scalars(
        query.where(
            Incident.status != "CLOSED",
            Incident.investigation_due_at.is_not(None),
            Incident.investigation_due_at < now,
        ).order_by(Incident.investigation_due_at)
    ).all()
    return render_template("dashboard.html", incidents=incidents, counts=counts, overdue=overdue[:8], risk_colors=RISK_COLORS)


@bp.get("/incidents")
@permission_required("incident.view")
def incidents():
    query = incident_query_for(current_user)
    search = request.args.get("q", "").strip()
    status = request.args.get("status", "").strip()
    risk = request.args.get("risk", "").strip()
    branch_id = request.args.get("branch_id", type=int)
    if search:
        like = f"%{search}%"
        query = query.where(
            or_(Incident.reference_no.ilike(like), Incident.location.ilike(like), Incident.description.ilike(like))
        )
    if status:
        query = query.where(Incident.status == status)
    if risk:
        query = query.where(Incident.risk_level == risk)
    if branch_id:
        query = query.where(Incident.branch_id == branch_id)
    page = max(request.args.get("page", 1, type=int), 1)
    per_page = 20
    total = db.session.scalar(db.select(func.count()).select_from(query.order_by(None).subquery())) or 0
    rows = db.session.scalars(query.order_by(Incident.created_at.desc()).offset((page - 1) * per_page).limit(per_page)).all()
    branches = db.session.scalars(db.select(Branch).where(Branch.is_active.is_(True)).order_by(Branch.name)).all()
    return render_template(
        "incidents/list.html", incidents=rows, total=total, page=page, per_page=per_page,
        branches=branches, risk_colors=RISK_COLORS,
    )


@bp.route("/incidents/new", methods=["GET", "POST"])
@permission_required("incident.create")
def incident_new():
    incident = Incident(reporter=current_user)
    if request.method == "POST":
        try:
            populate_incident(incident)
            db.session.add(incident)
            assign_reference(incident)
            action = request.form.get("action", "submit")
            if action == "draft":
                audit("INCIDENT_DRAFT_CREATED", "Incident", incident.id)
                db.session.commit()
                flash(f"Draft {incident.reference_no} saved.", "success")
            else:
                incident.submitted_at = utcnow()
                incident.assigned_manager = find_manager(incident)
                incident.assigned_qps = find_qps(incident)
                transition(incident, "SUBMITTED", "Occurrence report submitted", "Submitted within the electronic OVR cycle.")
                if incident.assigned_manager:
                    notify(incident.assigned_manager, "OVR requires manager review", f"{incident.reference_no} is awaiting your review.", incident)
                else:
                    notify_role("incident.qps_review", "OVR submitted without assigned manager", f"Please route {incident.reference_no}.", incident, incident.branch_id)
                db.session.commit()
                flash(f"Occurrence report {incident.reference_no} submitted confidentially.", "success")
            return redirect(url_for("main.incident_detail", incident_id=incident.id))
        except ValueError as exc:
            db.session.rollback()
            flash(str(exc), "danger")
    return render_template("incidents/form.html", incident=incident, **form_options())


@bp.route("/incidents/<int:incident_id>/edit", methods=["GET", "POST"])
@permission_required("incident.create")
def incident_edit(incident_id):
    incident = visible_incident(incident_id)
    if incident.status != "DRAFT" or (incident.reporter_id != current_user.id and current_user.role.scope != "all"):
        abort(403)
    if request.method == "POST":
        try:
            populate_incident(incident)
            action = request.form.get("action", "draft")
            if action == "submit":
                incident.submitted_at = utcnow()
                incident.assigned_manager = find_manager(incident)
                incident.assigned_qps = find_qps(incident)
                transition(incident, "SUBMITTED", "Occurrence report submitted")
                if incident.assigned_manager:
                    notify(incident.assigned_manager, "OVR requires manager review", f"{incident.reference_no} is awaiting your review.", incident)
            else:
                audit("INCIDENT_DRAFT_UPDATED", "Incident", incident.id)
            db.session.commit()
            flash("Draft submitted." if action == "submit" else "Draft updated.", "success")
            return redirect(url_for("main.incident_detail", incident_id=incident.id))
        except ValueError as exc:
            db.session.rollback()
            flash(str(exc), "danger")
    return render_template("incidents/form.html", incident=incident, **form_options())


@bp.get("/incidents/<int:incident_id>")
@permission_required("incident.view")
def incident_detail(incident_id):
    incident = visible_incident(incident_id)
    departments = db.session.scalars(
        db.select(Department).where(Department.branch_id == incident.branch_id, Department.is_active.is_(True)).order_by(Department.name)
    ).all()
    users = db.session.scalars(
        db.select(User).where(User.branch_id == incident.branch_id, User.is_active.is_(True)).order_by(User.full_name)
    ).all()
    if incident.is_anonymous and not current_user.can("incident.view_reporter_identity"):
        users = [user for user in users if user.id != incident.reporter_id]
    comments = [
        comment for comment in incident.comments
        if not comment.is_qps_only or current_user.can("incident.qps_review") or current_user.can("incident.close")
    ]
    return render_template(
        "incidents/detail.html",
        incident=incident,
        reporter_name=reporter_display(current_user, incident),
        departments=departments,
        users=users,
        comments=comments,
        contributory_factors=CONTRIBUTORY_FACTORS,
        sentinel_events=SENTINEL_EVENTS,
        harm_levels=HARM_LEVELS,
        likelihoods=LIKELIHOODS,
        risk_colors=RISK_COLORS,
    )


@bp.post("/incidents/<int:incident_id>/manager-review")
@permission_required("incident.manager_review")
def manager_review(incident_id):
    incident = visible_incident(incident_id)
    if incident.status != "SUBMITTED":
        flash("This report is not awaiting manager review.", "warning")
        return redirect(url_for("main.incident_detail", incident_id=incident.id))
    if current_user.role.scope == "department" and incident.department_id != current_user.department_id:
        abort(403)
    action = request.form.get("supervisor_action", "").strip()
    harm = request.form.get("consequence", "")
    likelihood = request.form.get("likelihood", "")
    if len(action) < 10 or harm not in HARM_LEVELS or likelihood not in LIKELIHOODS:
        flash("Manager action, consequence, and likelihood are required.", "danger")
        return redirect(url_for("main.incident_detail", incident_id=incident.id))
    factors = [item for item in request.form.getlist("contributing_factors") if item in CONTRIBUTORY_FACTORS]
    if len(factors) > 3:
        flash("Select no more than three contributing factors.", "danger")
        return redirect(url_for("main.incident_detail", incident_id=incident.id))
    score = risk_score(harm, likelihood)
    level = risk_level(score)
    incident.supervisor_action = action
    incident.patient_involved_confirmed = to_bool(request.form.get("patient_involved_confirmed"))
    incident.medical_record_documented = to_bool(request.form.get("medical_record_documented"))
    incident.sentinel_confirmed = bool(request.form.get("sentinel_confirmed"))
    incident.sentinel_event_type = request.form.get("sentinel_event_type", "").strip() or None
    incident.contributing_factors = factors
    incident.incident_causes = request.form.get("incident_causes", "").strip() or None
    incident.consequence = harm
    incident.harm_level = harm
    incident.likelihood = likelihood
    incident.risk_score = score
    incident.risk_level = level
    incident.risk_registered = level in {"High", "Extreme"}
    incident.manager_reviewed_at = utcnow()
    incident.investigation_due_at = due_date_for_risk(level, incident.manager_reviewed_at)
    if level == "Extreme":
        incident.action_plan_due_at = action_plan_due(incident.manager_reviewed_at)
    transition(incident, "QPS_REVIEW", "Manager review and initial investigation completed", action)
    if not incident.assigned_qps:
        incident.assigned_qps = find_qps(incident)
    if incident.assigned_qps:
        notify(incident.assigned_qps, "OVR ready for QPS analysis", f"{incident.reference_no} was graded {level} risk.", incident)
    else:
        notify_role("incident.qps_review", "OVR ready for QPS analysis", f"{incident.reference_no} was graded {level} risk.", incident)
    audit("MANAGER_REVIEW_COMPLETED", "Incident", incident.id, f"Risk score {score} ({level})")
    db.session.commit()
    flash("Manager review completed and sent to QPS.", "success")
    return redirect(url_for("main.incident_detail", incident_id=incident.id))


@bp.post("/incidents/<int:incident_id>/qps-review")
@permission_required("incident.qps_review")
def qps_review(incident_id):
    incident = visible_incident(incident_id)
    if incident.status != "QPS_REVIEW":
        flash("This report is not awaiting QPS analysis.", "warning")
        return redirect(url_for("main.incident_detail", incident_id=incident.id))
    analysis = request.form.get("qps_analysis", "").strip()
    recommendation = request.form.get("qps_recommendation", "").strip()
    if len(analysis) < 10:
        flash("QPS analysis is required.", "danger")
        return redirect(url_for("main.incident_detail", incident_id=incident.id))

    harm = request.form.get("consequence", incident.consequence)
    likelihood = request.form.get("likelihood", incident.likelihood)
    if harm in HARM_LEVELS and likelihood in LIKELIHOODS:
        incident.consequence = harm
        incident.harm_level = harm
        incident.likelihood = likelihood
        incident.risk_score = risk_score(harm, likelihood)
        incident.risk_level = risk_level(incident.risk_score)
        incident.risk_registered = incident.risk_level in {"High", "Extreme"}
        incident.investigation_due_at = due_date_for_risk(incident.risk_level, incident.manager_reviewed_at or utcnow())
        if incident.risk_level == "Extreme" and not incident.action_plan_due_at:
            incident.action_plan_due_at = action_plan_due(incident.manager_reviewed_at or utcnow())

    sentinel = bool(request.form.get("sentinel_confirmed")) or incident.report_type == "Sentinel Event"
    incident.sentinel_confirmed = sentinel
    incident.sentinel_event_type = request.form.get("sentinel_event_type", "").strip() or incident.sentinel_event_type
    incident.qps_analysis = analysis
    incident.qps_recommendation = recommendation or None
    incident.qps_reviewed_at = utcnow()
    further_action = bool(request.form.get("further_action"))

    if sentinel:
        transition(incident, "RCA_REQUIRED", "Sentinel event escalated for immediate RCA", recommendation)
        notify_role("incident.rca", "Sentinel event requires RCA", f"Immediate RCA is required for {incident.reference_no}.", incident, incident.branch_id)
        flash("Sentinel event escalated for immediate RCA.", "warning")
    elif further_action:
        description = request.form.get("action_description", "").strip()
        if len(description) < 10:
            flash("Describe the required department action.", "danger")
            return redirect(url_for("main.incident_detail", incident_id=incident.id))
        owner_id = request.form.get("action_owner_id", type=int)
        department_id = request.form.get("action_department_id", type=int) or incident.department_id
        owner = db.session.get(User, owner_id) if owner_id else None
        department = db.session.get(Department, department_id)
        if not department or department.branch_id != incident.branch_id or (owner and owner.branch_id != incident.branch_id):
            flash("Action department and owner must belong to the incident branch.", "danger")
            return redirect(url_for("main.incident_detail", incident_id=incident.id))
        kind = request.form.get("action_kind", "Corrective")
        strength = request.form.get("action_strength", "Strong")
        kind = kind if kind in {"Corrective", "Preventive"} else "Corrective"
        strength = strength if strength in {"Strong", "Intermediate", "Weak"} else "Strong"
        due = incident.investigation_due_at or due_date_for_risk(incident.risk_level or "Moderate", utcnow())
        item = ActionItem(
            incident=incident, department=department, owner=owner, description=description,
            kind=kind, strength=strength, due_at=due,
        )
        db.session.add(item)
        transition(incident, "ACTION_REQUIRED", "Further department action required", description)
        if owner:
            notify(owner, "Corrective action assigned", f"You have an action for {incident.reference_no}.", incident)
        flash("QPS analysis completed and corrective action assigned.", "success")
    else:
        incident.qps_closure_due_at = add_working_days(utcnow(), 4)
        transition(incident, "QPS_FINAL", "No further action required; ready for final closure", recommendation)
        flash("QPS analysis completed; the incident is ready for closure.", "success")
    audit("QPS_REVIEW_COMPLETED", "Incident", incident.id, f"Sentinel={sentinel}; Further action={further_action}")
    db.session.commit()
    return redirect(url_for("main.incident_detail", incident_id=incident.id))


@bp.post("/incidents/<int:incident_id>/rca")
@permission_required("incident.rca")
def complete_rca(incident_id):
    incident = visible_incident(incident_id)
    if incident.status != "RCA_REQUIRED":
        flash("This incident is not awaiting RCA.", "warning")
        return redirect(url_for("main.incident_detail", incident_id=incident.id))
    fields = {name: request.form.get(name, "").strip() for name in ["team_members", "problem_statement", "analysis", "root_causes", "recommendations"]}
    if any(len(value) < 10 for value in fields.values()):
        flash("Complete every RCA field with meaningful detail.", "danger")
        return redirect(url_for("main.incident_detail", incident_id=incident.id))
    if incident.rca:
        for key, value in fields.items():
            setattr(incident.rca, key, value)
        incident.rca.completed_by_id = current_user.id
        incident.rca.completed_at = utcnow()
    else:
        db.session.add(RCA(incident=incident, completed_by_id=current_user.id, **fields))
    incident.qps_closure_due_at = add_working_days(utcnow(), 4)
    transition(incident, "QPS_FINAL", "RCA completed; CAP and recommendations recorded", fields["recommendations"])
    if incident.assigned_qps:
        notify(incident.assigned_qps, "RCA ready for QPS closure", f"RCA for {incident.reference_no} is complete.", incident)
    audit("RCA_COMPLETED", "Incident", incident.id)
    db.session.commit()
    flash("RCA completed and sent for QPS final review.", "success")
    return redirect(url_for("main.incident_detail", incident_id=incident.id))


@bp.post("/incidents/<int:incident_id>/actions")
@permission_required("incident.action")
def add_action(incident_id):
    incident = visible_incident(incident_id)
    if incident.status == "CLOSED":
        abort(403)
    description = request.form.get("description", "").strip()
    if len(description) < 10:
        flash("Action description must contain at least 10 characters.", "danger")
        return redirect(url_for("main.incident_detail", incident_id=incident.id))
    owner = db.session.get(User, request.form.get("owner_id", type=int)) if request.form.get("owner_id", type=int) else None
    department = db.session.get(Department, request.form.get("department_id", type=int)) if request.form.get("department_id", type=int) else incident.department
    if not department or department.branch_id != incident.branch_id or (owner and owner.branch_id != incident.branch_id):
        flash("Action department and owner must belong to the incident branch.", "danger")
        return redirect(url_for("main.incident_detail", incident_id=incident.id))
    due_raw = request.form.get("due_at", "")
    try:
        due_at = datetime.fromisoformat(due_raw) if due_raw else incident.action_plan_due_at or incident.investigation_due_at
    except ValueError:
        due_at = incident.action_plan_due_at or incident.investigation_due_at
    kind = request.form.get("kind", "Corrective")
    strength = request.form.get("strength", "Strong")
    item = ActionItem(
        incident=incident, owner=owner, department=department,
        kind=kind if kind in {"Corrective", "Preventive"} else "Corrective",
        strength=strength if strength in {"Strong", "Intermediate", "Weak"} else "Strong",
        description=description, due_at=due_at,
    )
    db.session.add(item)
    audit("ACTION_CREATED", "ActionItem", None, f"Incident {incident.reference_no}: {description[:120]}")
    if owner:
        notify(owner, "OVR action assigned", f"You have been assigned an action for {incident.reference_no}.", incident)
    db.session.commit()
    flash("Corrective/preventive action added.", "success")
    return redirect(url_for("main.incident_detail", incident_id=incident.id))


@bp.post("/incidents/<int:incident_id>/actions/<int:action_id>/complete")
@permission_required("incident.action")
def complete_action(incident_id, action_id):
    incident = visible_incident(incident_id)
    item = db.get_or_404(ActionItem, action_id)
    if item.incident_id != incident.id or item.status == "COMPLETED":
        abort(404)
    if current_user.role.scope not in {"all", "branch"}:
        if item.owner_id == current_user.id:
            pass
        elif item.owner_id is None and item.department_id == current_user.department_id:
            pass
        else:
            abort(403)
    notes = request.form.get("completion_notes", "").strip()
    if len(notes) < 5:
        flash("Completion evidence/notes are required.", "danger")
        return redirect(url_for("main.incident_detail", incident_id=incident.id))
    item.status = "COMPLETED"
    item.completion_notes = notes
    item.completed_at = utcnow()
    audit("ACTION_COMPLETED", "ActionItem", item.id, notes[:180])
    db.session.flush()
    if incident.status == "ACTION_REQUIRED" and all(action.status == "COMPLETED" for action in incident.action_items):
        incident.qps_closure_due_at = add_working_days(utcnow(), 4)
        transition(incident, "QPS_FINAL", "All assigned actions completed", notes)
        if incident.assigned_qps:
            notify(incident.assigned_qps, "OVR actions completed", f"{incident.reference_no} is ready for final review.", incident)
    db.session.commit()
    flash("Action marked complete.", "success")
    return redirect(url_for("main.incident_detail", incident_id=incident.id))


@bp.post("/incidents/<int:incident_id>/close")
@permission_required("incident.close")
def close_incident(incident_id):
    incident = visible_incident(incident_id)
    if incident.status != "QPS_FINAL":
        flash("The incident is not ready for closure.", "warning")
        return redirect(url_for("main.incident_detail", incident_id=incident.id))
    if any(item.status != "COMPLETED" for item in incident.action_items):
        flash("All corrective/preventive actions must be completed before closure.", "danger")
        return redirect(url_for("main.incident_detail", incident_id=incident.id))
    if incident.risk_level == "Extreme" and not request.form.get("red_closure_approved"):
        flash("Red-risk closure requires recorded QPS Coordinator / Committee approval.", "danger")
        return redirect(url_for("main.incident_detail", incident_id=incident.id))
    feedback = request.form.get("final_feedback", "").strip()
    closure_reason = request.form.get("closure_reason", "").strip()
    if len(feedback) < 10 or len(closure_reason) < 5:
        flash("Final feedback and closure rationale are required.", "danger")
        return redirect(url_for("main.incident_detail", incident_id=incident.id))
    incident.final_feedback = feedback
    incident.closure_reason = closure_reason
    if incident.risk_level == "Extreme":
        incident.red_closure_approval_recorded = True
        incident.red_closure_approved_by_id = current_user.id
    incident.closed_at = utcnow()
    transition(incident, "CLOSED", "Incident closed in the OVR database", closure_reason)
    notify(incident.reporter, "OVR closed", f"{incident.reference_no} has been closed. Feedback: {feedback}", incident)
    if incident.assigned_manager and incident.assigned_manager_id != incident.reporter_id:
        notify(incident.assigned_manager, "OVR closure feedback", f"{incident.reference_no} has been closed.", incident)
    audit("INCIDENT_CLOSED", "Incident", incident.id, closure_reason)
    db.session.commit()
    flash("Incident closed and feedback sent to the reporter and manager.", "success")
    return redirect(url_for("main.incident_detail", incident_id=incident.id))


@bp.post("/incidents/<int:incident_id>/return")
@permission_required("incident.close")
def return_incident(incident_id):
    incident = visible_incident(incident_id)
    reason = request.form.get("reason", "").strip()
    if incident.status not in {"QPS_FINAL", "QPS_REVIEW"} or len(reason) < 10:
        flash("A clear return reason is required.", "danger")
        return redirect(url_for("main.incident_detail", incident_id=incident.id))
    transition(incident, "SUBMITTED", "Returned for incomplete/inappropriate action", reason)
    if incident.assigned_manager:
        notify(incident.assigned_manager, "OVR returned by QPS", f"{incident.reference_no}: {reason}", incident)
    audit("INCIDENT_RETURNED", "Incident", incident.id, reason)
    db.session.commit()
    flash("Incident returned to the manager for additional action.", "warning")
    return redirect(url_for("main.incident_detail", incident_id=incident.id))


@bp.post("/incidents/<int:incident_id>/comments")
@permission_required("incident.view")
def add_comment(incident_id):
    incident = visible_incident(incident_id)
    body = request.form.get("body", "").strip()
    if not body:
        flash("Comment cannot be empty.", "danger")
    else:
        qps_only = bool(request.form.get("is_qps_only")) and current_user.can("incident.qps_review")
        db.session.add(Comment(incident=incident, author_id=current_user.id, body=body, is_qps_only=qps_only))
        audit("COMMENT_ADDED", "Incident", incident.id, "QPS-only" if qps_only else "Shared")
        db.session.commit()
        flash("Comment added.", "success")
    return redirect(url_for("main.incident_detail", incident_id=incident.id))


@bp.post("/incidents/<int:incident_id>/attachments")
@permission_required("incident.view")
def upload_attachment(incident_id):
    incident = visible_incident(incident_id)
    uploaded = request.files.get("attachment")
    if not uploaded or not uploaded.filename:
        flash("Select a file to upload.", "danger")
        return redirect(url_for("main.incident_detail", incident_id=incident.id))
    original = secure_filename(uploaded.filename)
    extension = original.rsplit(".", 1)[-1].lower() if "." in original else ""
    if extension not in ALLOWED_EXTENSIONS:
        flash("Unsupported file type.", "danger")
        return redirect(url_for("main.incident_detail", incident_id=incident.id))
    stored = f"incident-{incident.id}-{uuid.uuid4().hex}.{extension}"
    target = os.path.join(upload_root(), stored)
    uploaded.save(target)
    size = os.path.getsize(target)
    attachment = Attachment(
        incident=incident, uploaded_by_id=current_user.id, original_name=original, stored_name=stored,
        mime_type=uploaded.mimetype, size_bytes=size,
    )
    db.session.add(attachment)
    audit("ATTACHMENT_UPLOADED", "Incident", incident.id, original)
    db.session.commit()
    flash("Attachment uploaded securely.", "success")
    return redirect(url_for("main.incident_detail", incident_id=incident.id))


@bp.get("/incidents/<int:incident_id>/attachments/<int:attachment_id>")
@permission_required("incident.view")
def download_attachment(incident_id, attachment_id):
    incident = visible_incident(incident_id)
    attachment = db.get_or_404(Attachment, attachment_id)
    if attachment.incident_id != incident.id:
        abort(404)
    audit("ATTACHMENT_VIEWED", "Attachment", attachment.id, attachment.original_name)
    db.session.commit()
    return send_from_directory(
        upload_root(), attachment.stored_name, download_name=attachment.original_name,
        as_attachment=request.args.get("download") == "1",
    )


def _pdf_footer(canvas, doc):
    canvas.saveState()
    canvas.setFillColor(colors.HexColor("#9b1c1c"))
    canvas.setFont("Helvetica-Bold", 8)
    canvas.drawString(18 * mm, 10 * mm, "CONFIDENTIAL - OCCURRENCE / VARIANCE REPORT")
    canvas.setFillColor(colors.HexColor("#475569"))
    canvas.drawRightString(192 * mm, 10 * mm, f"Page {doc.page}")
    canvas.restoreState()


@bp.get("/incidents/<int:incident_id>/pdf")
@permission_required("incident.export")
def incident_pdf(incident_id):
    incident = visible_incident(incident_id)
    stream = io.BytesIO()
    styles = getSampleStyleSheet()
    styles.add(ParagraphStyle(name="OVRTitle", parent=styles["Title"], textColor=colors.HexColor("#0f4c5c"), fontSize=18, alignment=TA_CENTER, spaceAfter=8))
    styles.add(ParagraphStyle(name="Section", parent=styles["Heading2"], textColor=colors.white, backColor=colors.HexColor("#0f4c5c"), borderPadding=5, fontSize=10, spaceBefore=8, spaceAfter=6))
    styles.add(ParagraphStyle(name="Small", parent=styles["BodyText"], fontSize=8.5, leading=11))
    doc = SimpleDocTemplate(stream, pagesize=A4, rightMargin=18 * mm, leftMargin=18 * mm, topMargin=16 * mm, bottomMargin=18 * mm)
    story = [
        Paragraph("AL WATTAN MEDICAL GROUP", styles["OVRTitle"]),
        Paragraph("Electronic Occurrence / Variance Report (OVR)", styles["Heading2"]),
        Paragraph(f"Reference: <b>{incident.reference_no}</b> &nbsp;&nbsp; Status: <b>{incident.status_label}</b>", styles["BodyText"]),
        Spacer(1, 5),
    ]

    def section(title, rows):
        story.append(Paragraph(title, styles["Section"]))
        data = []
        for label, value in rows:
            safe_value = str(value if value not in (None, "") else "Not provided").replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
            data.append([Paragraph(f"<b>{label}</b>", styles["Small"]), Paragraph(safe_value, styles["Small"])])
        table = Table(data, colWidths=[48 * mm, 108 * mm], repeatRows=0)
        table.setStyle(TableStyle([
            ("VALIGN", (0, 0), (-1, -1), "TOP"), ("GRID", (0, 0), (-1, -1), 0.3, colors.HexColor("#cbd5e1")),
            ("BACKGROUND", (0, 0), (0, -1), colors.HexColor("#f1f5f9")), ("LEFTPADDING", (0, 0), (-1, -1), 6),
            ("RIGHTPADDING", (0, 0), (-1, -1), 6), ("TOPPADDING", (0, 0), (-1, -1), 5), ("BOTTOMPADDING", (0, 0), (-1, -1), 5),
        ]))
        story.append(table)

    section("1. Occurrence Details", [
        ("Branch", incident.branch.name), ("Reporting Department", incident.department.name),
        ("Incident Date / Time", incident.occurred_at_display), ("Location", incident.location),
        ("Reporter", reporter_display(current_user, incident)), ("Person Involved", incident.person_type),
        ("Person Name", incident.person_name), ("Patient MRN", incident.patient_mrn),
        ("Other Involved Departments", ", ".join(item.name for item in incident.involved_departments) or "None"),
    ])
    section("2. Classification and Factual Report", [
        ("Classification", incident.classification_category), ("Selected Items", ", ".join(incident.classification_items)),
        ("Other Detail", incident.classification_other), ("Report Type", incident.report_type),
        ("Injury", f"{'Yes' if incident.injury_occurred else 'No'} {incident.injury_type or ''}"),
        ("Harm / Likelihood", f"{incident.harm_level or 'Not graded'} / {incident.likelihood or 'Not graded'}"),
        ("Medication Harm Category", incident.medication_harm_category),
        ("Factual Description", incident.description), ("Immediate Action", incident.immediate_action),
    ])
    section("3. Manager Investigation", [
        ("Initial Action / Treatment", incident.supervisor_action),
        ("Medical Record Documented", "Yes" if incident.medical_record_documented else "No" if incident.medical_record_documented is not None else None),
        ("Sentinel Event", "Yes" if incident.sentinel_confirmed else "No"),
        ("Sentinel Type", incident.sentinel_event_type), ("Contributing Factors", ", ".join(incident.contributing_factors)),
        ("Incident Causes", incident.incident_causes),
        ("Risk", f"{incident.risk_score or '-'} - {incident.risk_level or 'Not graded'}"),
        ("Investigation Due", incident.investigation_due_at.strftime("%Y-%m-%d") if incident.investigation_due_at else None),
    ])
    section("4. QPS, RCA and Corrective Actions", [
        ("QPS Analysis", incident.qps_analysis), ("QPS Recommendation", incident.qps_recommendation),
        ("RCA Root Causes", incident.rca.root_causes if incident.rca else None),
        ("RCA Recommendations", incident.rca.recommendations if incident.rca else None),
        ("Action Items", "\n".join(f"[{item.status}] {item.kind}: {item.description}" for item in incident.action_items) or "None"),
    ])
    section("5. QPS Closure and Feedback", [
        ("Final Feedback", incident.final_feedback), ("Closure Rationale", incident.closure_reason),
        ("Closed At", incident.closed_at.strftime("%Y-%m-%d %H:%M") if incident.closed_at else None),
    ])
    story.append(PageBreak())
    story.append(Paragraph("Audit-ready Workflow History", styles["Section"]))
    history = [["Date / Time", "Actor", "Action", "Status"]]
    for log in incident.workflow_logs:
        history.append([
            log.created_at.strftime("%Y-%m-%d %H:%M"),
            "Anonymous reporter" if incident.is_anonymous and log.actor_id == incident.reporter_id and not current_user.can("incident.view_reporter_identity") else log.actor.full_name if log.actor else "System",
            log.action, log.to_status.replace("_", " ").title(),
        ])
    history_table = Table(history, colWidths=[32 * mm, 36 * mm, 70 * mm, 28 * mm], repeatRows=1)
    history_table.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#0f4c5c")), ("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
        ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), ("FONTSIZE", (0, 0), (-1, -1), 7.5),
        ("GRID", (0, 0), (-1, -1), 0.3, colors.HexColor("#cbd5e1")), ("VALIGN", (0, 0), (-1, -1), "TOP"),
        ("LEFTPADDING", (0, 0), (-1, -1), 4), ("RIGHTPADDING", (0, 0), (-1, -1), 4),
        ("TOPPADDING", (0, 0), (-1, -1), 4), ("BOTTOMPADDING", (0, 0), (-1, -1), 4),
    ]))
    story.append(history_table)
    doc.build(story, onFirstPage=_pdf_footer, onLaterPages=_pdf_footer)
    audit("INCIDENT_PDF_EXPORTED", "Incident", incident.id)
    db.session.commit()
    response = make_response(stream.getvalue())
    response.headers["Content-Type"] = "application/pdf"
    response.headers["Content-Disposition"] = f'inline; filename="{incident.reference_no}.pdf"'
    return response


@bp.get("/notifications")
@login_required
def notifications():
    rows = db.session.scalars(
        db.select(Notification).where(Notification.user_id == current_user.id).order_by(Notification.created_at.desc()).limit(100)
    ).all()
    return render_template("notifications.html", notifications=rows)


@bp.post("/notifications/read-all")
@login_required
def notifications_read_all():
    db.session.execute(
        db.update(Notification).where(Notification.user_id == current_user.id, Notification.read_at.is_(None)).values(read_at=utcnow())
    )
    db.session.commit()
    return redirect(url_for("main.notifications"))


@bp.get("/reports")
@permission_required("reports.view")
def reports():
    query = incident_query_for(current_user)
    year = request.args.get("year", utcnow().year, type=int)
    quarter = request.args.get("quarter", 0, type=int)
    query = query.where(func.extract("year", Incident.incident_date) == year)
    if quarter in {1, 2, 3, 4}:
        start_month = (quarter - 1) * 3 + 1
        end_month = start_month + 2
        query = query.where(func.extract("month", Incident.incident_date).between(start_month, end_month))
    rows = db.session.scalars(query.order_by(Incident.incident_date.desc())).all()
    by_risk = {level: sum(1 for item in rows if item.risk_level == level) for level in ["Low", "Moderate", "High", "Extreme"]}
    by_status = {}
    by_category = {}
    by_branch = {}
    for item in rows:
        by_status[item.status_label] = by_status.get(item.status_label, 0) + 1
        by_category[item.classification_category] = by_category.get(item.classification_category, 0) + 1
        by_branch[item.branch.name] = by_branch.get(item.branch.name, 0) + 1
    return render_template(
        "reports.html", rows=rows, year=year, quarter=quarter, by_risk=by_risk,
        by_status=by_status, by_category=by_category, by_branch=by_branch, risk_colors=RISK_COLORS,
    )


@bp.get("/reports/export.csv")
@permission_required("reports.view")
def reports_csv():
    query = incident_query_for(current_user)
    year = request.args.get("year", utcnow().year, type=int)
    quarter = request.args.get("quarter", 0, type=int)
    query = query.where(func.extract("year", Incident.incident_date) == year)
    if quarter in {1, 2, 3, 4}:
        query = query.where(func.extract("month", Incident.incident_date).between((quarter - 1) * 3 + 1, (quarter - 1) * 3 + 3))
    rows = db.session.scalars(query.order_by(Incident.incident_date)).all()
    output = io.StringIO()
    writer = csv.writer(output)
    writer.writerow(["Reference", "Branch", "Department", "Incident Date", "Category", "Report Type", "Risk Score", "Risk Level", "Status", "Closed At"])
    for item in rows:
        writer.writerow([
            item.reference_no, item.branch.name, item.department.name, item.incident_date.isoformat(),
            item.classification_category, item.report_type, item.risk_score or "", item.risk_level or "",
            item.status_label, item.closed_at.isoformat() if item.closed_at else "",
        ])
    audit("QUARTERLY_REPORT_EXPORTED", "Report", f"{year}-Q{quarter or 'ALL'}", f"{len(rows)} rows")
    db.session.commit()
    response = make_response("\ufeff" + output.getvalue())
    response.headers["Content-Type"] = "text/csv; charset=utf-8"
    response.headers["Content-Disposition"] = f'attachment; filename="AWMG_OVR_{year}_Q{quarter or "ALL"}.csv"'
    return response
