from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError

REQUEST_STATES = [
    ("pending", "Pending"),
    ("supervisor_approve", "Supervisor Outside Approve"),
    ("supervisor_reject", "Supervisor Outside Reject"),
]

VEHICLE_TYPES = [
    ("company_car", "Company Car"),
    ("personal_car", "Personal Car"),
    ("personal_bike", "Personal Bike"),
]


class XenoOrrRequest(models.Model):
    """An Outside Request (field trip). State only tracks the outside-trip
    approval itself (Section 14's first stage); everything from
    "Reimbursement Pending" onward lives on xeno.orr.reimbursement, since
    a single trip can carry more than one reimbursement ATTEMPT (Section
    7) -- current_reimbursement_id/reimbursement_state below surface the
    latest attempt for list views and the dashboard without callers having
    to know about the attempt-chain themselves."""

    _name = "xeno.orr.request"
    _description = "Outside Request"
    _inherit = ["mail.thread"]
    _order = "id desc"

    name = fields.Char(default="New", copy=False, readonly=True)
    employee_id = fields.Many2one(
        "hr.employee", required=True, tracking=True,
        default=lambda self: self.env.user.employee_id.id)
    supervisor_id = fields.Many2one("hr.employee", string="Supervisor", required=True, tracking=True)
    department_head_id = fields.Many2one("hr.employee", string="Head of Department", tracking=True)

    datetime_leaving = fields.Datetime(string="Date & Time Leaving", required=True, tracking=True)
    return_to_office = fields.Boolean(string="Return to Office", default=True)
    datetime_returning = fields.Datetime(string="Time Returning")
    no_return_reason = fields.Text(string="Reason (Not Returning)")

    vehicle_type = fields.Selection(VEHICLE_TYPES, required=True, tracking=True)
    license_plate = fields.Char()

    has_companion = fields.Boolean(string="Add Companion")
    companion_ids = fields.Many2many("hr.employee", string="Companion(s)")

    stop_ids = fields.One2many("xeno.orr.stop", "request_id", string="Places & Purpose")

    manual_distance_km = fields.Float(
        string="Distance (km, manual fallback)",
        help="Used for Fuel reimbursement only if GPS-recorded distance below is unavailable.")
    map_link = fields.Char(string="Map Screenshot/Link (fallback)")
    gps_distance_km = fields.Float(string="GPS Route Distance (km)", compute="_compute_distance", store=True)
    distance_km = fields.Float(
        string="Distance Used for Reimbursement (km)", compute="_compute_distance", store=True,
        help="GPS route distance when available, otherwise the manual fallback distance.")

    state = fields.Selection(REQUEST_STATES, default="pending", required=True, tracking=True, copy=False)
    outside_reject_reason = fields.Char(copy=False)
    supervisor_decided_by = fields.Many2one("res.users", readonly=True, copy=False)
    supervisor_decided_at = fields.Datetime(readonly=True, copy=False)

    reimbursement_ids = fields.One2many("xeno.orr.reimbursement", "request_id")
    current_reimbursement_id = fields.Many2one(
        "xeno.orr.reimbursement", compute="_compute_current_reimbursement", store=True)
    reimbursement_state = fields.Selection(
        related="current_reimbursement_id.state", string="Reimbursement Status", store=True)
    can_ask_reimbursement = fields.Boolean(compute="_compute_can_ask_reimbursement")
    can_supervisor_decide = fields.Boolean(compute="_compute_can_supervisor_decide")

    @api.depends_context("uid")
    @api.depends("supervisor_id")
    def _compute_can_supervisor_decide(self):
        for rec in self:
            rec.can_supervisor_decide = rec._xeno_is_supervisor() or rec._xeno_is_hr()

    @api.model_create_multi
    def create(self, vals_list):
        for vals in vals_list:
            if vals.get("name", "New") == "New":
                vals["name"] = self.env["ir.sequence"].next_by_code("xeno.orr.request") or "New"
        records = super().create(vals_list)
        records._xeno_notify_outside_submitted()
        return records

    @api.depends("stop_ids.gps_captured", "stop_ids.latitude", "stop_ids.longitude", "manual_distance_km")
    def _compute_distance(self):
        for rec in self:
            rec.gps_distance_km = self.env["xeno.orr.stop"].xeno_compute_route_distance_km(rec.stop_ids)
            rec.distance_km = rec.gps_distance_km or rec.manual_distance_km or 0.0

    @api.depends("reimbursement_ids.attempt_number")
    def _compute_current_reimbursement(self):
        for rec in self:
            rec.current_reimbursement_id = rec.reimbursement_ids.filtered("is_current")[:1]

    @api.depends("state", "current_reimbursement_id", "current_reimbursement_id.state")
    def _compute_can_ask_reimbursement(self):
        for rec in self:
            if rec.state != "supervisor_approve":
                rec.can_ask_reimbursement = False
            elif not rec.current_reimbursement_id:
                rec.can_ask_reimbursement = True
            else:
                rec.can_ask_reimbursement = rec.current_reimbursement_id.state == "supervisor_reject"

    def _xeno_approvers_for_employee(self, employee):
        """Auto-fills Supervisor and Department Head, both freely
        overridable before sending (same "suggest, don't lock" convention
        xeno_overtime uses for its own two approver fields).

        Supervisor defaults to step 1 of this employee's Approver
        Configuration chain (xeno.leave.approver, the same setup HR already
        maintains for Leave requests, task #48) -- Outside Request approval
        follows that one shared configuration instead of a separate,
        independently-maintained rule. Falls back to the employee's plain
        reporting manager (as a default value only -- any HR/Admin can
        still act via _xeno_is_hr(), same as today) when step 1 names no
        specific person, whether because no chain is configured at all for
        their department/employee override (e.g. Marketing/Sales/
        Logistics/Procurement/IT, still unconfigured per the Leave approver
        rollout) or because step 1 is itself a real configured "any
        HR/Admin" step."""
        vals = {}
        if employee:
            configured = self._xeno_configured_approver(employee)
            vals["supervisor_id"] = (configured or employee.parent_id).id
            vals["department_head_id"] = employee.department_id.manager_id.id
        return vals

    def _xeno_configured_approver(self, employee):
        """Step 1 of this employee's Approver Configuration chain, taken
        literally -- see build_chain()'s own docstring for the
        employee-override/department/HR-fallback resolution order. If step
        1 names no specific person (either a real configured "any HR/Admin"
        step, or the chain is entirely unconfigured and step 1 is already
        the synthetic "any HR" fallback), this returns an empty recordset
        rather than skipping ahead to a later, more-senior step in the
        chain -- ORR has one approval stage, so it mirrors whichever person
        (or lack of one) HR actually put first, never a later step."""
        steps = self.env["xeno.leave.approver"].build_chain(employee)
        first_user_id = steps[0]["approver_user_id"] if steps else None
        if not first_user_id:
            return self.env["hr.employee"]
        return self.env["hr.employee"].search([("user_id", "=", first_user_id)], limit=1)

    @api.onchange("employee_id")
    def _onchange_employee_id_xeno_fill_approvers(self):
        for field, value in self._xeno_approvers_for_employee(self.employee_id).items():
            setattr(self, field, value)

    @api.constrains("vehicle_type", "license_plate")
    def _check_license_plate(self):
        for rec in self:
            if rec.vehicle_type in ("personal_car", "personal_bike") and not rec.license_plate:
                raise ValidationError(_("License Plate is required for a Personal Car/Bike trip."))

    @api.constrains("return_to_office", "datetime_returning", "no_return_reason")
    def _check_return_fields(self):
        for rec in self:
            if rec.return_to_office and not rec.datetime_returning:
                raise ValidationError(_("Time Returning is required when returning to the office."))
            if not rec.return_to_office and not rec.no_return_reason:
                raise ValidationError(_("A reason is required when not returning to the office."))

    @api.constrains("stop_ids")
    def _check_has_stops(self):
        for rec in self:
            if not rec.stop_ids:
                raise ValidationError(_("Add at least one Place & Purpose stop before sending."))

    def _xeno_is_supervisor(self):
        self.ensure_one()
        return self.env.user.employee_id == self.supervisor_id

    def _xeno_is_hr(self):
        return self.env.is_superuser() or self.env.user.has_group("hr_holidays.group_hr_holidays_user")

    def write(self, vals):
        transition_fields = {
            "state", "outside_reject_reason", "supervisor_decided_by", "supervisor_decided_at",
        }
        if not self.env.su and not self._xeno_is_hr() and set(vals) & transition_fields:
            raise UserError(_(
                "Use the Approve/Reject actions to change an Outside Request's status -- "
                "it can't be edited directly."))
        return super().write(vals)

    # ------------------------------------------------------------ actions

    def action_supervisor_approve(self):
        for rec in self:
            if rec.state != "pending":
                raise UserError(_("Only a Pending request can be approved."))
            if not (rec._xeno_is_supervisor() or rec._xeno_is_hr()):
                raise UserError(_("Only the configured Supervisor can approve this request."))
            rec.sudo().write({
                "state": "supervisor_approve",
                "supervisor_decided_by": self.env.user.id,
                "supervisor_decided_at": fields.Datetime.now(),
            })
            rec.message_post(body=_("Outside Request approved by Supervisor: %s", self.env.user.name))
            rec._xeno_notify_outside_approved()

    def action_supervisor_reject(self, reason=False):
        for rec in self:
            if rec.state != "pending":
                raise UserError(_("Only a Pending request can be rejected."))
            if not (rec._xeno_is_supervisor() or rec._xeno_is_hr()):
                raise UserError(_("Only the configured Supervisor can reject this request."))
            if not reason:
                raise UserError(_("A reason is required to reject an Outside Request."))
            rec.sudo().write({
                "state": "supervisor_reject",
                "outside_reject_reason": reason,
                "supervisor_decided_by": self.env.user.id,
                "supervisor_decided_at": fields.Datetime.now(),
            })
            rec.message_post(body=_("Outside Request rejected by Supervisor: %s", self.env.user.name))

    def action_xeno_open_stops(self):
        """Opens the GPS trip-stops dialog (own OWL client action, same
        pattern as xeno_attendance's location preview) -- kept separate
        from the plain editable stop_ids list embedded on this form so a
        stop's GPS point can be captured live from the browser, which a
        plain list-view button can't do on its own."""
        self.ensure_one()
        return {
            "type": "ir.actions.client",
            "tag": "xeno_orr_stops",
            "name": _("Trip Stops – %s", self.name),
            "target": "new",
            "params": {"request_id": self.id},
        }

    def action_ask_reimbursement(self, vals):
        self.ensure_one()
        if not self.can_ask_reimbursement:
            raise UserError(_(
                "Reimbursement can only be requested after the Supervisor has approved "
                "this Outside Request (and isn't already awaiting a decision)."))
        if self.env.user.employee_id != self.employee_id and not self._xeno_is_hr():
            raise UserError(_("Only the requesting employee can ask for reimbursement."))
        return self._xeno_create_reimbursement(vals, attempt_number=1)

    def _xeno_create_reimbursement(self, vals, attempt_number):
        """Shared by action_ask_reimbursement() (attempt 1) and
        xeno.orr.reimbursement.action_resend() (attempt 2+). `vals`:
        {'reimbursement_type': 'fuel'|'other', 'line_ids': [(0,0,{...}), ...]}
        -- fuel's amount is computed here server-side (Section 6: "Requestor
        does not manually enter the fuel amount"), never trusted from the client."""
        self.ensure_one()
        reimbursement_type = vals.get("reimbursement_type")
        if reimbursement_type == "fuel" and self.vehicle_type == "company_car":
            raise UserError(_("Fuel reimbursement isn't available for a Company Car trip."))
        create_vals = {
            "request_id": self.id,
            "attempt_number": attempt_number,
            "reimbursement_type": reimbursement_type,
        }
        if reimbursement_type == "fuel":
            create_vals.update({
                "fuel_distance_km": self.distance_km,
                "fuel_rate": self.env["xeno.orr.config"].sudo().get_rate(),
            })
        else:
            create_vals["line_ids"] = vals.get("line_ids", [])
        reimbursement = self.env["xeno.orr.reimbursement"].create(create_vals)
        reimbursement.message_post(body=_("Reimbursement requested (attempt %s).", attempt_number))
        reimbursement._xeno_notify_reimbursement_submitted()
        return reimbursement
