"""
setup_fsm_doctypes.py — Create Field Service Management doctypes for Gigafibre.

Inspired by Odoo OCA Field Service (fsm.location, fsm.equipment, fsm.order)
and adapted for Frappe/ERPNext with ISP/telecom-specific fields.

Run inside the bench container:
  bench --site <site-name> execute setup_fsm_doctypes.create_all

Or via docker:
  docker compose exec backend bench --site frontend execute setup_fsm_doctypes.create_all
"""

import frappe


def create_all():
    _create_service_location()
    _create_service_subscription()
    _create_equipment_move_log()
    _create_service_equipment()
    _create_job_equipment_item()
    _create_job_material_used()
    _create_job_checklist_item()
    _create_job_photo()
    _create_checklist_template()
    _create_dispatch_job_assistant()
    _extend_dispatch_job()
    _create_contract_benefit()
    _create_service_contract()
    _add_quotation_custom_fields()
    _add_billing_frequency_fields()
    frappe.db.commit()
    print("✓ FSM doctypes created successfully.")


# ─────────────────────────────────────────────────────────────────────────────
# Service Location — Customer premises where service is delivered
# ─────────────────────────────────────────────────────────────────────────────

def _create_service_location():
    if frappe.db.exists("DocType", "Service Location"):
        print("  Service Location already exists — skipping.")
        return

    doc = frappe.get_doc({
        "doctype": "DocType",
        "name": "Service Location",
        "module": "Dispatch",
        "custom": 1,
        "autoname": "LOC-.#####",
        "track_changes": 1,
        "fields": [
            # ── Client ──────────────────────────────────────────────────────
            {"fieldname": "customer", "fieldtype": "Link", "label": "Client",
             "options": "Customer", "reqd": 1, "in_list_view": 1},
            {"fieldname": "customer_name", "fieldtype": "Data", "label": "Nom du client",
             "fetch_from": "customer.customer_name", "read_only": 1},
            {"fieldname": "col_loc1", "fieldtype": "Column Break"},
            {"fieldname": "location_name", "fieldtype": "Data", "label": "Nom du lieu",
             "in_list_view": 1},
            {"fieldname": "status", "fieldtype": "Select", "label": "Statut",
             "options": "Active\nInactive\nPending Install",
             "default": "Pending Install", "in_list_view": 1},

            # ── Adresse ─────────────────────────────────────────────────────
            {"fieldname": "sec_address", "fieldtype": "Section Break", "label": "Adresse"},
            {"fieldname": "address_line", "fieldtype": "Small Text", "label": "Adresse", "reqd": 1},
            {"fieldname": "city", "fieldtype": "Data", "label": "Ville"},
            {"fieldname": "postal_code", "fieldtype": "Data", "label": "Code postal"},
            {"fieldname": "province", "fieldtype": "Data", "label": "Province", "default": "QC"},
            {"fieldname": "col_geo", "fieldtype": "Column Break"},
            {"fieldname": "longitude", "fieldtype": "Float", "label": "Longitude", "precision": "7"},
            {"fieldname": "latitude", "fieldtype": "Float", "label": "Latitude", "precision": "7"},

            # ── Infra technique ──────────────────────────────────────────────
            {"fieldname": "sec_infra", "fieldtype": "Section Break", "label": "Infrastructure technique"},
            {"fieldname": "connection_type", "fieldtype": "Select", "label": "Type de raccordement",
             "options": "\nFibre FTTH\nFibre FTTB\nCable coaxial\nDSL\nSans-fil\nAutre"},
            {"fieldname": "olt_port", "fieldtype": "Data", "label": "Port OLT"},
            {"fieldname": "col_infra", "fieldtype": "Column Break"},
            {"fieldname": "network_id", "fieldtype": "Data", "label": "ID réseau (VLAN/PPPoE)"},
            {"fieldname": "network_notes", "fieldtype": "Small Text", "label": "Notes réseau"},

            # ── Contact sur place ────────────────────────────────────────────
            {"fieldname": "sec_contact", "fieldtype": "Section Break", "label": "Contact sur place"},
            {"fieldname": "contact_name", "fieldtype": "Data", "label": "Nom contact"},
            {"fieldname": "contact_phone", "fieldtype": "Data", "label": "Téléphone"},
            {"fieldname": "col_access", "fieldtype": "Column Break"},
            {"fieldname": "access_notes", "fieldtype": "Small Text", "label": "Notes d'accès",
             "description": "Code porte, chien, instructions spéciales..."},
        ],
        "permissions": [
            {"role": "System Manager", "read": 1, "write": 1, "create": 1, "delete": 1},
            {"role": "All", "read": 1, "write": 1, "create": 1, "delete": 1},
        ],
    })
    doc.insert(ignore_permissions=True)
    print("  ✓ Service Location created.")


# ─────────────────────────────────────────────────────────────────────────────
# Equipment Move Log — Child table for equipment movement history
# ─────────────────────────────────────────────────────────────────────────────

def _create_equipment_move_log():
    if frappe.db.exists("DocType", "Equipment Move Log"):
        print("  Equipment Move Log already exists — skipping.")
        return

    doc = frappe.get_doc({
        "doctype": "DocType",
        "name": "Equipment Move Log",
        "module": "Dispatch",
        "custom": 1,
        "istable": 1,
        "fields": [
            {"fieldname": "move_date", "fieldtype": "Date", "label": "Date", "default": "Today", "in_list_view": 1},
            {"fieldname": "from_location", "fieldtype": "Link", "label": "De", "options": "Service Location", "in_list_view": 1},
            {"fieldname": "to_location", "fieldtype": "Link", "label": "Vers", "options": "Service Location", "in_list_view": 1},
            {"fieldname": "reason", "fieldtype": "Select", "label": "Raison",
             "options": "Installation\nRemplacement\nRéparation\nRetour\nTransfert", "in_list_view": 1},
            {"fieldname": "technician", "fieldtype": "Data", "label": "Technicien"},
            {"fieldname": "dispatch_job", "fieldtype": "Link", "label": "Job", "options": "Dispatch Job"},
            {"fieldname": "notes", "fieldtype": "Small Text", "label": "Notes"},
        ],
    })
    doc.insert(ignore_permissions=True)
    print("  ✓ Equipment Move Log created.")


# ─────────────────────────────────────────────────────────────────────────────
# Service Equipment — Physical devices at customer premises
# ─────────────────────────────────────────────────────────────────────────────

def _create_service_equipment():
    if frappe.db.exists("DocType", "Service Equipment"):
        print("  Service Equipment already exists — skipping.")
        return

    doc = frappe.get_doc({
        "doctype": "DocType",
        "name": "Service Equipment",
        "module": "Dispatch",
        "custom": 1,
        "autoname": "EQP-.#####",
        "track_changes": 1,
        "fields": [
            # ── Identification ───────────────────────────────────────────────
            {"fieldname": "equipment_type", "fieldtype": "Select", "label": "Type",
             "options": "ONT\nModem\nRouteur\nDécodeur TV\nTéléphone IP\nSwitch\nAmplificateur\nAP WiFi\nCâble/Connecteur\nAutre",
             "reqd": 1, "in_list_view": 1},
            {"fieldname": "brand", "fieldtype": "Data", "label": "Marque"},
            {"fieldname": "model", "fieldtype": "Data", "label": "Modèle"},
            {"fieldname": "col_id", "fieldtype": "Column Break"},
            {"fieldname": "serial_number", "fieldtype": "Data", "label": "Numéro de série",
             "reqd": 1, "unique": 1, "in_list_view": 1},
            {"fieldname": "mac_address", "fieldtype": "Data", "label": "Adresse MAC"},
            {"fieldname": "barcode", "fieldtype": "Data", "label": "Code-barres"},

            # ── Emplacement ──────────────────────────────────────────────────
            {"fieldname": "sec_location", "fieldtype": "Section Break", "label": "Emplacement"},
            {"fieldname": "customer", "fieldtype": "Link", "label": "Client", "options": "Customer", "in_list_view": 1},
            {"fieldname": "service_location", "fieldtype": "Link", "label": "Lieu de service",
             "options": "Service Location"},
            {"fieldname": "col_status", "fieldtype": "Column Break"},
            {"fieldname": "status", "fieldtype": "Select", "label": "Statut",
             "options": "En inventaire\nActif\nEn diagnostic\nDéfectueux\nRetourné\nPerdu",
             "default": "En inventaire", "in_list_view": 1},
            {"fieldname": "ownership", "fieldtype": "Select", "label": "Propriété",
             "options": "Gigafibre\nClient", "default": "Gigafibre"},

            # ── Abonnement lié ───────────────────────────────────────────────
            {"fieldname": "sec_sub", "fieldtype": "Section Break", "label": "Abonnement"},
            {"fieldname": "subscription", "fieldtype": "Link", "label": "Abonnement",
             "options": "Service Subscription"},

            # ── Dates ────────────────────────────────────────────────────────
            {"fieldname": "sec_dates", "fieldtype": "Section Break", "label": "Dates"},
            {"fieldname": "purchase_date", "fieldtype": "Date", "label": "Date d'achat"},
            {"fieldname": "installation_date", "fieldtype": "Date", "label": "Date d'installation"},
            {"fieldname": "col_dates", "fieldtype": "Column Break"},
            {"fieldname": "warranty_end", "fieldtype": "Date", "label": "Fin garantie"},

            # ── Configuration réseau ─────────────────────────────────────────
            {"fieldname": "sec_config", "fieldtype": "Section Break", "label": "Configuration"},
            {"fieldname": "ip_address", "fieldtype": "Data", "label": "Adresse IP"},
            {"fieldname": "firmware_version", "fieldtype": "Data", "label": "Version firmware"},
            {"fieldname": "col_config", "fieldtype": "Column Break"},
            {"fieldname": "login_user", "fieldtype": "Data", "label": "Utilisateur"},
            {"fieldname": "login_password", "fieldtype": "Password", "label": "Mot de passe"},

            # ── Historique ───────────────────────────────────────────────────
            {"fieldname": "sec_history", "fieldtype": "Section Break", "label": "Historique des déplacements"},
            {"fieldname": "move_log", "fieldtype": "Table", "label": "Mouvements",
             "options": "Equipment Move Log"},

            # ── Notes ────────────────────────────────────────────────────────
            {"fieldname": "sec_notes", "fieldtype": "Section Break", "label": "Notes"},
            {"fieldname": "notes", "fieldtype": "Small Text", "label": "Notes"},
            {"fieldname": "photo", "fieldtype": "Attach Image", "label": "Photo"},
        ],
        "permissions": [
            {"role": "System Manager", "read": 1, "write": 1, "create": 1, "delete": 1},
            {"role": "All", "read": 1, "write": 1, "create": 1, "delete": 1},
        ],
    })
    doc.insert(ignore_permissions=True)
    print("  ✓ Service Equipment created.")


# ─────────────────────────────────────────────────────────────────────────────
# Service Subscription — Active service plans at locations
# ─────────────────────────────────────────────────────────────────────────────

def _create_service_subscription():
    if frappe.db.exists("DocType", "Service Subscription"):
        print("  Service Subscription already exists — skipping.")
        return

    doc = frappe.get_doc({
        "doctype": "DocType",
        "name": "Service Subscription",
        "module": "Dispatch",
        "custom": 1,
        "autoname": "SUB-.#####",
        "track_changes": 1,
        "fields": [
            # ── Client ──────────────────────────────────────────────────────
            {"fieldname": "customer", "fieldtype": "Link", "label": "Client",
             "options": "Customer", "reqd": 1, "in_list_view": 1},
            {"fieldname": "customer_name", "fieldtype": "Data", "label": "Nom du client",
             "fetch_from": "customer.customer_name", "read_only": 1},
            {"fieldname": "service_location", "fieldtype": "Link", "label": "Lieu de service",
             "options": "Service Location", "reqd": 1},
            {"fieldname": "col_sub1", "fieldtype": "Column Break"},
            {"fieldname": "status", "fieldtype": "Select", "label": "Statut",
             "options": "Actif\nSuspendu\nAnnulé\nEn attente",
             "default": "En attente", "in_list_view": 1},

            # ── Forfait ──────────────────────────────────────────────────────
            {"fieldname": "sec_plan", "fieldtype": "Section Break", "label": "Forfait"},
            {"fieldname": "service_category", "fieldtype": "Select", "label": "Catégorie",
             "options": "Internet\nIPTV\nVoIP\nBundle\nHébergement\nAutre",
             "reqd": 1, "in_list_view": 1},
            {"fieldname": "plan_name", "fieldtype": "Data", "label": "Nom du forfait",
             "in_list_view": 1},
            {"fieldname": "col_plan", "fieldtype": "Column Break"},
            {"fieldname": "speed_down", "fieldtype": "Int", "label": "Débit ↓ (Mbps)"},
            {"fieldname": "speed_up", "fieldtype": "Int", "label": "Débit ↑ (Mbps)"},

            # ── Tarification ─────────────────────────────────────────────────
            {"fieldname": "sec_price", "fieldtype": "Section Break", "label": "Tarification"},
            {"fieldname": "monthly_price", "fieldtype": "Currency", "label": "Prix mensuel"},
            {"fieldname": "billing_cycle", "fieldtype": "Select", "label": "Cycle de facturation",
             "options": "Mensuel\nTrimestriel\nAnnuel", "default": "Mensuel"},
            {"fieldname": "col_price", "fieldtype": "Column Break"},
            {"fieldname": "contract_duration", "fieldtype": "Int", "label": "Durée contrat (mois)"},
            {"fieldname": "promo_end", "fieldtype": "Date", "label": "Fin promotion"},

            # ── Dates ────────────────────────────────────────────────────────
            {"fieldname": "sec_subdates", "fieldtype": "Section Break", "label": "Dates"},
            {"fieldname": "start_date", "fieldtype": "Date", "label": "Début", "reqd": 1},
            {"fieldname": "end_date", "fieldtype": "Date", "label": "Fin"},
            {"fieldname": "col_subdates", "fieldtype": "Column Break"},
            {"fieldname": "cancellation_date", "fieldtype": "Date", "label": "Date d'annulation"},
            {"fieldname": "cancellation_reason", "fieldtype": "Small Text", "label": "Raison annulation"},

            # ── Notes ────────────────────────────────────────────────────────
            {"fieldname": "sec_subnotes", "fieldtype": "Section Break", "label": "Notes"},
            {"fieldname": "notes", "fieldtype": "Small Text", "label": "Notes"},
        ],
        "permissions": [
            {"role": "System Manager", "read": 1, "write": 1, "create": 1, "delete": 1},
            {"role": "All", "read": 1, "write": 1, "create": 1, "delete": 1},
        ],
    })
    doc.insert(ignore_permissions=True)
    print("  ✓ Service Subscription created.")


# ─────────────────────────────────────────────────────────────────────────────
# Child tables for enhanced Dispatch Job
# ─────────────────────────────────────────────────────────────────────────────

def _create_job_equipment_item():
    if frappe.db.exists("DocType", "Job Equipment Item"):
        print("  Job Equipment Item already exists — skipping.")
        return
    doc = frappe.get_doc({
        "doctype": "DocType", "name": "Job Equipment Item",
        "module": "Dispatch", "custom": 1, "istable": 1,
        "fields": [
            {"fieldname": "equipment", "fieldtype": "Link", "label": "Équipement",
             "options": "Service Equipment", "in_list_view": 1},
            {"fieldname": "serial_number", "fieldtype": "Data", "label": "N/S",
             "fetch_from": "equipment.serial_number", "read_only": 1, "in_list_view": 1},
            {"fieldname": "equipment_type", "fieldtype": "Data", "label": "Type",
             "fetch_from": "equipment.equipment_type", "read_only": 1, "in_list_view": 1},
            {"fieldname": "action", "fieldtype": "Select", "label": "Action",
             "options": "Installé\nRetiré\nRemplacé\nDiagnostiqué\nConfiguré", "in_list_view": 1},
            {"fieldname": "notes", "fieldtype": "Data", "label": "Notes"},
        ],
    })
    doc.insert(ignore_permissions=True)
    print("  ✓ Job Equipment Item created.")


def _create_job_material_used():
    if frappe.db.exists("DocType", "Job Material Used"):
        print("  Job Material Used already exists — skipping.")
        return
    doc = frappe.get_doc({
        "doctype": "DocType", "name": "Job Material Used",
        "module": "Dispatch", "custom": 1, "istable": 1,
        "fields": [
            {"fieldname": "item_name", "fieldtype": "Data", "label": "Article", "reqd": 1, "in_list_view": 1},
            {"fieldname": "quantity", "fieldtype": "Float", "label": "Quantité", "default": "1", "in_list_view": 1},
            {"fieldname": "unit", "fieldtype": "Data", "label": "Unité", "default": "pcs"},
            {"fieldname": "notes", "fieldtype": "Data", "label": "Notes", "in_list_view": 1},
        ],
    })
    doc.insert(ignore_permissions=True)
    print("  ✓ Job Material Used created.")


def _create_job_checklist_item():
    if frappe.db.exists("DocType", "Job Checklist Item"):
        print("  Job Checklist Item already exists — skipping.")
        return
    doc = frappe.get_doc({
        "doctype": "DocType", "name": "Job Checklist Item",
        "module": "Dispatch", "custom": 1, "istable": 1,
        "fields": [
            {"fieldname": "task", "fieldtype": "Data", "label": "Tâche", "reqd": 1, "in_list_view": 1},
            {"fieldname": "is_done", "fieldtype": "Check", "label": "Fait", "in_list_view": 1},
            {"fieldname": "notes", "fieldtype": "Data", "label": "Notes", "in_list_view": 1},
        ],
    })
    doc.insert(ignore_permissions=True)
    print("  ✓ Job Checklist Item created.")


def _create_job_photo():
    if frappe.db.exists("DocType", "Job Photo"):
        print("  Job Photo already exists — skipping.")
        return
    doc = frappe.get_doc({
        "doctype": "DocType", "name": "Job Photo",
        "module": "Dispatch", "custom": 1, "istable": 1,
        "fields": [
            {"fieldname": "photo", "fieldtype": "Attach Image", "label": "Photo"},
            {"fieldname": "caption", "fieldtype": "Data", "label": "Légende", "in_list_view": 1},
            {"fieldname": "taken_at", "fieldtype": "Datetime", "label": "Prise le"},
        ],
    })
    doc.insert(ignore_permissions=True)
    print("  ✓ Job Photo created.")


# ─────────────────────────────────────────────────────────────────────────────
# Checklist Template — Default checklists per job type
# ─────────────────────────────────────────────────────────────────────────────

def _create_checklist_template():
    # Child table for template items
    if not frappe.db.exists("DocType", "Checklist Template Item"):
        frappe.get_doc({
            "doctype": "DocType", "name": "Checklist Template Item",
            "module": "Dispatch", "custom": 1, "istable": 1,
            "fields": [
                {"fieldname": "task", "fieldtype": "Data", "label": "Tâche", "reqd": 1, "in_list_view": 1},
                {"fieldname": "sort_order", "fieldtype": "Int", "label": "Ordre", "in_list_view": 1},
            ],
        }).insert(ignore_permissions=True)
        print("  ✓ Checklist Template Item created.")

    if frappe.db.exists("DocType", "Checklist Template"):
        print("  Checklist Template already exists — skipping.")
        return

    doc = frappe.get_doc({
        "doctype": "DocType", "name": "Checklist Template",
        "module": "Dispatch", "custom": 1,
        "autoname": "field:template_name",
        "fields": [
            {"fieldname": "template_name", "fieldtype": "Data", "label": "Nom", "reqd": 1, "unique": 1},
            {"fieldname": "job_type", "fieldtype": "Select", "label": "Type de job",
             "options": "Installation\nRéparation\nMaintenance\nRetrait\nDépannage\nAutre"},
            {"fieldname": "items", "fieldtype": "Table", "label": "Tâches",
             "options": "Checklist Template Item"},
        ],
        "permissions": [
            {"role": "System Manager", "read": 1, "write": 1, "create": 1, "delete": 1},
        ],
    })
    doc.insert(ignore_permissions=True)
    print("  ✓ Checklist Template created.")


# ─────────────────────────────────────────────────────────────────────────────
# Dispatch Job Assistant — child table : équipe (lead + assistants) sur une tâche
# Déjà présent sur le site live (créé hors repo) — versionné ici pour reproductibilité.
# ─────────────────────────────────────────────────────────────────────────────

def _create_dispatch_job_assistant():
    """Child table for crew/assistants on a Dispatch Job (le lead = assigned_tech)."""
    if frappe.db.exists("DocType", "Dispatch Job Assistant"):
        print("  Dispatch Job Assistant already exists — skipping.")
        return
    doc = frappe.get_doc({
        "doctype": "DocType", "name": "Dispatch Job Assistant",
        "module": "Dispatch", "custom": 1, "istable": 1,
        "fields": [
            {"fieldname": "tech_id", "fieldtype": "Data", "label": "Technicien (id)", "in_list_view": 1},
            {"fieldname": "tech_name", "fieldtype": "Data", "label": "Nom", "in_list_view": 1},
            {"fieldname": "duration_h", "fieldtype": "Float", "label": "Durée (h)", "in_list_view": 1,
             "description": "Temps de l'assistant sur la tâche (peut être < la durée totale)"},
            {"fieldname": "note", "fieldtype": "Data", "label": "Rôle / note"},
            {"fieldname": "pinned", "fieldtype": "Check", "label": "Épinglé", "default": "0",
             "description": "Si coché : bloque la lane de l'assistant + compte dans sa charge (anti double-booking)"},
        ],
    })
    doc.insert(ignore_permissions=True)
    print("  ✓ Dispatch Job Assistant created.")


# ─────────────────────────────────────────────────────────────────────────────
# Extend Dispatch Job — Add FSM fields to existing doctype
# ─────────────────────────────────────────────────────────────────────────────

def _extend_dispatch_job():
    """Add custom fields to the existing Dispatch Job doctype."""
    new_fields = [
        # Client & Lieu
        {"dt": "Dispatch Job", "fieldname": "customer", "fieldtype": "Link",
         "label": "Client", "options": "Customer", "insert_after": "subject"},
        {"dt": "Dispatch Job", "fieldname": "service_location", "fieldtype": "Link",
         "label": "Lieu de service", "options": "Service Location", "insert_after": "customer"},
        # Type
        {"dt": "Dispatch Job", "fieldname": "job_type", "fieldtype": "Select",
         "label": "Type de travail",
         "options": "\nInstallation\nRéparation\nMaintenance\nRetrait\nDépannage\nAutre",
         "insert_after": "service_location"},
        {"dt": "Dispatch Job", "fieldname": "source_issue", "fieldtype": "Link",
         "label": "Ticket source", "options": "Issue", "insert_after": "job_type"},
        {"dt": "Dispatch Job", "fieldname": "depends_on", "fieldtype": "Link",
         "label": "Dépend de", "options": "Dispatch Job", "insert_after": "source_issue"},
        {"dt": "Dispatch Job", "fieldname": "parent_job", "fieldtype": "Link",
         "label": "Tâche parente", "options": "Dispatch Job", "insert_after": "depends_on"},
        {"dt": "Dispatch Job", "fieldname": "on_open_webhook", "fieldtype": "Data",
         "label": "Webhook n8n (ouverture)", "insert_after": "parent_job",
         "description": "URL webhook n8n déclenché à l'ouverture/assignation de la tâche"},
        {"dt": "Dispatch Job", "fieldname": "on_close_webhook", "fieldtype": "Data",
         "label": "Webhook n8n (fermeture)", "insert_after": "on_open_webhook",
         "description": "URL webhook n8n déclenché à la complétion de la tâche"},
        {"dt": "Dispatch Job", "fieldname": "step_order", "fieldtype": "Int",
         "label": "Ordre d'étape", "insert_after": "on_close_webhook",
         "description": "Position dans la séquence du projet (1, 2, 3...)"},
        # Équipe / assistants (le LEAD reste assigned_tech ; assistants = renfort)
        {"dt": "Dispatch Job", "fieldname": "assistants", "fieldtype": "Table",
         "label": "Assistants (équipe)", "options": "Dispatch Job Assistant", "insert_after": "step_order",
         "description": "Techniciens en renfort sur la tâche ; le responsable reste « assigned_tech »"},
        # Equipment
        {"dt": "Dispatch Job", "fieldname": "sec_equipment", "fieldtype": "Section Break",
         "label": "Équipements", "insert_after": "tags"},
        {"dt": "Dispatch Job", "fieldname": "equipment_items", "fieldtype": "Table",
         "label": "Équipements", "options": "Job Equipment Item", "insert_after": "sec_equipment"},
        # Materials
        {"dt": "Dispatch Job", "fieldname": "sec_materials", "fieldtype": "Section Break",
         "label": "Matériaux utilisés", "insert_after": "equipment_items"},
        {"dt": "Dispatch Job", "fieldname": "materials_used", "fieldtype": "Table",
         "label": "Matériaux", "options": "Job Material Used", "insert_after": "sec_materials"},
        # Time tracking
        {"dt": "Dispatch Job", "fieldname": "sec_time", "fieldtype": "Section Break",
         "label": "Suivi temps", "insert_after": "materials_used"},
        {"dt": "Dispatch Job", "fieldname": "actual_start", "fieldtype": "Datetime",
         "label": "Début réel", "insert_after": "sec_time"},
        {"dt": "Dispatch Job", "fieldname": "actual_end", "fieldtype": "Datetime",
         "label": "Fin réelle", "insert_after": "actual_start"},
        {"dt": "Dispatch Job", "fieldname": "travel_time_min", "fieldtype": "Int",
         "label": "Temps trajet (min)", "insert_after": "actual_end"},
        # Checklist
        {"dt": "Dispatch Job", "fieldname": "sec_checklist", "fieldtype": "Section Break",
         "label": "Checklist", "insert_after": "travel_time_min"},
        {"dt": "Dispatch Job", "fieldname": "checklist", "fieldtype": "Table",
         "label": "Checklist", "options": "Job Checklist Item", "insert_after": "sec_checklist"},
        # Completion
        {"dt": "Dispatch Job", "fieldname": "sec_completion", "fieldtype": "Section Break",
         "label": "Complétion", "insert_after": "checklist"},
        {"dt": "Dispatch Job", "fieldname": "completion_notes", "fieldtype": "Text",
         "label": "Notes de complétion", "insert_after": "sec_completion"},
        {"dt": "Dispatch Job", "fieldname": "customer_signature", "fieldtype": "Attach Image",
         "label": "Signature client", "insert_after": "completion_notes"},
        {"dt": "Dispatch Job", "fieldname": "photos", "fieldtype": "Table",
         "label": "Photos", "options": "Job Photo", "insert_after": "customer_signature"},
        # Publish state
        {"dt": "Dispatch Job", "fieldname": "published", "fieldtype": "Check",
         "label": "Publié", "default": "0", "insert_after": "status",
         "description": "Horaire publié et envoyé au technicien"},
    ]

    for field_def in new_fields:
        fieldname = field_def["fieldname"]
        if frappe.db.exists("Custom Field", {"dt": "Dispatch Job", "fieldname": fieldname}):
            continue
        frappe.get_doc({"doctype": "Custom Field", **field_def}).insert(ignore_permissions=True)
        print(f"  + Dispatch Job.{fieldname}")

    print("  ✓ Dispatch Job extended with FSM fields.")

    # ── Extend Dispatch Technician with resource type fields ──
    tech_fields = [
        {"dt": "Dispatch Technician", "fieldname": "resource_type", "fieldtype": "Select",
         "label": "Type de ressource", "options": "human\nmaterial", "default": "human",
         "insert_after": "full_name"},
        {"dt": "Dispatch Technician", "fieldname": "resource_category", "fieldtype": "Select",
         "label": "Catégorie ressource",
         "options": "\nVéhicule\nOutil\nSalle\nÉquipement\nNacelle\nGrue\nFusionneuse\nOTDR",
         "insert_after": "resource_type"},
        {"dt": "Dispatch Technician", "fieldname": "extra_shifts", "fieldtype": "Small Text",
         "label": "Extra Shifts (JSON)", "insert_after": "weekly_schedule", "hidden": 1},
    ]
    for field_def in tech_fields:
        fieldname = field_def["fieldname"]
        if frappe.db.exists("Custom Field", {"dt": "Dispatch Technician", "fieldname": fieldname}):
            continue
        frappe.get_doc({"doctype": "Custom Field", **field_def}).insert(ignore_permissions=True)
        print(f"  + Dispatch Technician.{fieldname}")

    print("  ✓ Dispatch Technician extended with resource type fields.")


# ─────────────────────────────────────────────────────────────────────────────
# Contract Benefit — Child table for benefits granted at installation
# Example: "Installation 0$ sur entente 24 mois (prix régulier 288$)"
# ─────────────────────────────────────────────────────────────────────────────

def _create_contract_benefit():
    if frappe.db.exists("DocType", "Contract Benefit"):
        print("  Contract Benefit already exists — skipping.")
        return

    doc = frappe.get_doc({
        "doctype": "DocType",
        "name": "Contract Benefit",
        "module": "Dispatch",
        "custom": 1,
        "istable": 1,
        "fields": [
            {"fieldname": "description", "fieldtype": "Data", "label": "Description",
             "reqd": 1, "in_list_view": 1, "columns": 3},
            {"fieldname": "regular_price", "fieldtype": "Currency", "label": "Prix régulier",
             "reqd": 1, "in_list_view": 1, "columns": 1,
             "description": "Coût réel du service/installation (ex: 288$)"},
            {"fieldname": "granted_price", "fieldtype": "Currency", "label": "Prix accordé",
             "in_list_view": 1, "columns": 1, "default": "0",
             "description": "Prix facturé au client (ex: 0$ = gratuit)"},
            {"fieldname": "benefit_value", "fieldtype": "Currency", "label": "Valeur avantage",
             "read_only": 1, "in_list_view": 1, "columns": 1,
             "description": "regular_price - granted_price (calculé automatiquement)"},
            {"fieldname": "monthly_recognition", "fieldtype": "Currency",
             "label": "Reconnaissance mensuelle", "read_only": 1, "in_list_view": 1, "columns": 1,
             "description": "benefit_value / durée contrat mois (compensé par mois)"},
            {"fieldname": "months_recognized", "fieldtype": "Int",
             "label": "Mois reconnus", "read_only": 1, "default": "0",
             "description": "Nombre de mois d'abonnement écoulés depuis le début"},
            {"fieldname": "remaining_value", "fieldtype": "Currency",
             "label": "Valeur résiduelle", "read_only": 1,
             "description": "Montant restant non compensé (pour résiliation anticipée)"},
        ],
    })
    doc.insert(ignore_permissions=True)
    print("  ✓ Contract Benefit child table created.")


# ─────────────────────────────────────────────────────────────────────────────
# Service Contract — Engagement/Entente de service
# Handles both residential and commercial contracts with different
# termination penalty rules:
#   - Résidentiel: max mois courant + pro-rata des avantages non compensés
#   - Commercial: paiement de toutes les mensualités restantes
# ─────────────────────────────────────────────────────────────────────────────

def _create_service_contract():
    if frappe.db.exists("DocType", "Service Contract"):
        print("  Service Contract already exists — skipping.")
        return

    doc = frappe.get_doc({
        "doctype": "DocType",
        "name": "Service Contract",
        "module": "Dispatch",
        "custom": 1,
        "autoname": "CTR-.#####",
        "track_changes": 1,
        "fields": [
            # ── Parties ─────────────────────────────────────────────────────
            {"fieldname": "customer", "fieldtype": "Link", "label": "Client",
             "options": "Customer", "reqd": 1, "in_list_view": 1},
            {"fieldname": "customer_name", "fieldtype": "Data", "label": "Nom du client",
             "fetch_from": "customer.customer_name", "read_only": 1},
            {"fieldname": "col_contract1", "fieldtype": "Column Break"},
            {"fieldname": "contract_type", "fieldtype": "Select", "label": "Type de contrat",
             "options": "Résidentiel\nCommercial", "default": "Résidentiel",
             "reqd": 1, "in_list_view": 1,
             "description": "Résidentiel: pénalité = avantages non compensés. Commercial: totalité des mensualités restantes."},
            {"fieldname": "status", "fieldtype": "Select", "label": "Statut",
             "options": "Brouillon\nEnvoyé\nActif\nRésilié\nComplété\nExpiré",
             "default": "Brouillon", "in_list_view": 1, "reqd": 1},

            # ── Durée ───────────────────────────────────────────────────────
            {"fieldname": "sec_duration", "fieldtype": "Section Break", "label": "Durée de l'engagement"},
            {"fieldname": "start_date", "fieldtype": "Date", "label": "Début du contrat"},
            {"fieldname": "duration_months", "fieldtype": "Int", "label": "Durée (mois)",
             "default": "24", "reqd": 1,
             "description": "Durée de l'engagement en mois (ex: 24)"},
            {"fieldname": "col_duration", "fieldtype": "Column Break"},
            {"fieldname": "end_date", "fieldtype": "Date", "label": "Fin prévue",
             "read_only": 1, "description": "Calculé: start_date + duration_months"},
            {"fieldname": "months_elapsed", "fieldtype": "Int", "label": "Mois écoulés",
             "read_only": 1, "default": "0"},
            {"fieldname": "months_remaining", "fieldtype": "Int", "label": "Mois restants",
             "read_only": 1},

            # ── Abonnement mensuel ─────────────────────────────────────────
            {"fieldname": "sec_subscription", "fieldtype": "Section Break", "label": "Abonnement"},
            {"fieldname": "monthly_rate", "fieldtype": "Currency", "label": "Mensualité",
             "reqd": 1, "description": "Montant mensuel de l'abonnement (ex: 79.95$)"},
            {"fieldname": "subscription", "fieldtype": "Link", "label": "Abonnement ERPNext",
             "options": "Subscription",
             "description": "Lien vers le Subscription ERPNext associé"},
            {"fieldname": "col_sub", "fieldtype": "Column Break"},
            {"fieldname": "service_location", "fieldtype": "Link", "label": "Lieu de service",
             "options": "Service Location"},
            {"fieldname": "quotation", "fieldtype": "Link", "label": "Devis d'origine",
             "options": "Quotation"},

            # ── Avantages accordés (child table) ──────────────────────────
            {"fieldname": "sec_benefits", "fieldtype": "Section Break",
             "label": "Avantages accordés",
             "description": "Avantages gratuits compensés au pro-rata sur la durée du contrat"},
            {"fieldname": "benefits", "fieldtype": "Table", "label": "Avantages",
             "options": "Contract Benefit"},
            {"fieldname": "total_benefit_value", "fieldtype": "Currency",
             "label": "Valeur totale des avantages", "read_only": 1},
            {"fieldname": "total_remaining_value", "fieldtype": "Currency",
             "label": "Valeur résiduelle totale", "read_only": 1,
             "description": "Somme des avantages non encore compensés"},

            # ── Mention première facture ──────────────────────────────────
            {"fieldname": "sec_invoice_note", "fieldtype": "Section Break",
             "label": "Mention sur facture"},
            {"fieldname": "invoice_note", "fieldtype": "Small Text",
             "label": "Mention sur la première facture",
             "description": "Ex: Installation 0$ sur entente de 24 mois. (Prix régulier 288$) Chaque mois d'abonnement reconnaît 12$ de compensé."},

            # ── Signature / Acceptation ───────────────────────────────────
            {"fieldname": "sec_signature", "fieldtype": "Section Break", "label": "Acceptation"},
            {"fieldname": "acceptance_method", "fieldtype": "Select",
             "label": "Méthode d'acceptation",
             "options": "\nDocuSeal\nJWT SMS\nSignature physique",
             "description": "DocuSeal = e-signature via sign.gigafibre.ca, JWT SMS = confirmation par lien SMS"},
            {"fieldname": "docuseal_template_id", "fieldtype": "Int",
             "label": "Template DocuSeal ID"},
            {"fieldname": "docuseal_submission_id", "fieldtype": "Int",
             "label": "DocuSeal Submission ID", "read_only": 1},
            {"fieldname": "col_sig", "fieldtype": "Column Break"},
            {"fieldname": "signed_at", "fieldtype": "Datetime", "label": "Signé le", "read_only": 1},
            {"fieldname": "signed_by", "fieldtype": "Data", "label": "Signé par", "read_only": 1},
            {"fieldname": "signature_proof", "fieldtype": "Small Text",
             "label": "Preuve d'acceptation", "read_only": 1,
             "description": "IP, user-agent, horodatage, lien DocuSeal"},

            # ── Résiliation ───────────────────────────────────────────────
            {"fieldname": "sec_termination", "fieldtype": "Section Break",
             "label": "Résiliation anticipée", "collapsible": 1},
            {"fieldname": "terminated_at", "fieldtype": "Date", "label": "Date de résiliation",
             "read_only": 1},
            {"fieldname": "termination_reason", "fieldtype": "Small Text",
             "label": "Raison de résiliation"},
            {"fieldname": "col_term", "fieldtype": "Column Break"},
            {"fieldname": "termination_fee_benefits", "fieldtype": "Currency",
             "label": "Pénalité avantages (résidentiel)", "read_only": 1,
             "description": "Valeur résiduelle des avantages non compensés"},
            {"fieldname": "termination_fee_remaining", "fieldtype": "Currency",
             "label": "Pénalité mensualités (commercial)", "read_only": 1,
             "description": "Mois restants × mensualité"},
            {"fieldname": "termination_fee_total", "fieldtype": "Currency",
             "label": "Pénalité totale", "read_only": 1, "bold": 1},
            {"fieldname": "termination_invoice", "fieldtype": "Link",
             "label": "Facture de résiliation", "options": "Sales Invoice", "read_only": 1},
        ],
        "permissions": [
            {"role": "System Manager", "read": 1, "write": 1, "create": 1, "delete": 1},
            {"role": "Dispatch User", "read": 1, "write": 1, "create": 1},
        ],
    })
    doc.insert(ignore_permissions=True)
    print("  ✓ Service Contract doctype created.")


# ─────────────────────────────────────────────────────────────────────────────
# Custom fields on Quotation — DocuSeal + wizard integration
# ─────────────────────────────────────────────────────────────────────────────

def _add_quotation_custom_fields():
    # Only create fields that don't exist — leave existing ones alone so we
    # never clobber a Long Text with Text, or overwrite migration-era labels.
    # The wizard_* fields and accepted_by_client already exist on the live
    # ERPNext site (created during legacy import); only the DocuSeal trio is
    # missing and needed for the "Soumission TARGO" print format.
    fields = [
        {
            "fieldname": "custom_docuseal_signing_url",
            "label": "DocuSeal Signing URL",
            "fieldtype": "Data",
            "read_only": 1,
            "insert_after": "terms",
            "description": "Populated by targo-hub after DocuSeal submission — used by Soumission TARGO QR code",
        },
        {
            "fieldname": "custom_docuseal_envelope_id",
            "label": "DocuSeal Submission ID",
            "fieldtype": "Data",
            "read_only": 1,
            "insert_after": "custom_docuseal_signing_url",
        },
        {
            "fieldname": "custom_quote_type",
            "label": "Type de contrat",
            "fieldtype": "Select",
            "options": "\nRésidentiel\nCommercial",
            "insert_after": "custom_docuseal_envelope_id",
        },
        # Fallback creation for fresh environments — skipped if already present.
        {
            "fieldname": "wizard_steps",
            "label": "Étapes du projet (JSON)",
            "fieldtype": "Long Text",
            "read_only": 1,
            "insert_after": "custom_quote_type",
        },
        {
            "fieldname": "wizard_context",
            "label": "Contexte du projet (JSON)",
            "fieldtype": "Long Text",
            "read_only": 1,
            "insert_after": "wizard_steps",
        },
        {
            "fieldname": "accepted_by_client",
            "label": "Accepté par le client",
            "fieldtype": "Check",
            "read_only": 1,
            "insert_after": "wizard_context",
        },
    ]

    for f in fields:
        fname = f["fieldname"]
        if frappe.db.exists("Custom Field", {"dt": "Quotation", "fieldname": fname}):
            print(f"  Custom Field Quotation.{fname} already exists — skipping")
            continue
        frappe.get_doc({
            "doctype": "Custom Field",
            "dt": "Quotation",
            **f,
        }).insert(ignore_permissions=True)
        print(f"  Created Custom Field Quotation.{fname}")


# ─────────────────────────────────────────────────────────────────────────────
# Billing frequency — marks items as One-time / Monthly / Annual so the
# Soumission TARGO print format can split totals (recurring vs. initial fees).
# Lives on Item (source of truth) and Quotation Item (inherited via fetch_from,
# but overridable per line). Also propagated to Sales Invoice Item so the same
# information flows through the invoice pipeline.
# ─────────────────────────────────────────────────────────────────────────────

def _add_billing_frequency_fields():
    BILLING_OPTIONS = "One-time\nMonthly\nAnnual"

    targets = [
        {
            "dt": "Item",
            "insert_after": "item_group",
            "fetch_from": None,
            "read_only": 0,
            "default": "One-time",
        },
        {
            "dt": "Quotation Item",
            "insert_after": "item_name",
            "fetch_from": "item_code.custom_billing_frequency",
            "read_only": 0,
            "default": None,
        },
        {
            "dt": "Sales Invoice Item",
            "insert_after": "item_name",
            "fetch_from": "item_code.custom_billing_frequency",
            "read_only": 0,
            "default": None,
        },
        {
            "dt": "Sales Order Item",
            "insert_after": "item_name",
            "fetch_from": "item_code.custom_billing_frequency",
            "read_only": 0,
            "default": None,
        },
    ]

    for t in targets:
        if frappe.db.exists("Custom Field", {"dt": t["dt"], "fieldname": "custom_billing_frequency"}):
            print(f"  Custom Field {t['dt']}.custom_billing_frequency already exists — skipping")
            continue
        payload = {
            "doctype": "Custom Field",
            "dt": t["dt"],
            "fieldname": "custom_billing_frequency",
            "label": "Billing Frequency",
            "fieldtype": "Select",
            "options": BILLING_OPTIONS,
            "insert_after": t["insert_after"],
            "in_list_view": 0,
            "translatable": 0,
        }
        if t.get("fetch_from"):
            payload["fetch_from"] = t["fetch_from"]
            payload["fetch_if_empty"] = 1
        if t.get("default"):
            payload["default"] = t["default"]
        frappe.get_doc(payload).insert(ignore_permissions=True)
        print(f"  Created Custom Field {t['dt']}.custom_billing_frequency")
