#!/usr/bin/env python3
import hashlib
import hmac
import os
import secrets
import subprocess
import tkinter as tk
from tkinter import messagebox, ttk

import pymysql


def load_env(path="/etc/conf.d/crm"):
    env = {}
    try:
        with open(path, "r", encoding="utf-8") as fh:
            for line in fh:
                line = line.strip()
                if not line or line.startswith("#") or "=" not in line:
                    continue
                key, value = line.split("=", 1)
                env[key] = value.strip().strip('"').strip("'")
    except FileNotFoundError:
        pass
    return env


CFG = load_env()


def cfg(name, default=""):
    return os.environ.get(name) or CFG.get(name) or default


def db():
    return pymysql.connect(
        host=cfg("MYSQL_HOST"),
        port=int(cfg("MYSQL_PORT", "3306")),
        user=cfg("MYSQL_USER"),
        password=cfg("MYSQL_PASSWORD"),
        database=cfg("MYSQL_DATABASE"),
        charset="utf8mb4",
        autocommit=True,
        cursorclass=pymysql.cursors.DictCursor,
        connect_timeout=5,
    )


SCHEMA = [
    """
    create table if not exists crm_users (
        id bigint unsigned not null auto_increment primary key,
        username varchar(128) not null unique,
        password_hash varchar(255) not null,
        display_name varchar(255) not null default '',
        role varchar(64) not null default 'agent',
        active tinyint(1) not null default 1,
        last_login timestamp null,
        created_at timestamp not null default current_timestamp
    ) charset=utf8mb4
    """,
    """
    create table if not exists crm_contacts (
        id bigint unsigned not null auto_increment primary key,
        name varchar(255) not null default '',
        phone varchar(64) not null default '',
        company varchar(255) not null default '',
        status varchar(64) not null default 'new',
        notes text not null,
        assigned_user_id bigint unsigned null,
        updated_at timestamp not null default current_timestamp on update current_timestamp,
        created_at timestamp not null default current_timestamp,
        key phone_idx (phone),
        key assigned_user_idx (assigned_user_id)
    ) charset=utf8mb4
    """,
    """
    create table if not exists crm_calls (
        id bigint unsigned not null auto_increment primary key,
        contact_id bigint unsigned null,
        user_id bigint unsigned null,
        phone varchar(64) not null default '',
        direction varchar(16) not null default 'out',
        status varchar(64) not null default 'planned',
        note text not null,
        created_at timestamp not null default current_timestamp,
        key contact_idx (contact_id),
        key user_idx (user_id),
        key phone_idx (phone)
    ) charset=utf8mb4
    """,
]


def hash_password(password):
    iterations = 200000
    salt = secrets.token_hex(16)
    digest = hashlib.pbkdf2_hmac("sha256", password.encode(), salt.encode(), iterations).hex()
    return f"pbkdf2_sha256${iterations}${salt}${digest}"


def verify_password(password, stored):
    if stored.startswith("pbkdf2_sha256$"):
        try:
            _name, iterations, salt, digest = stored.split("$", 3)
            got = hashlib.pbkdf2_hmac("sha256", password.encode(), salt.encode(), int(iterations)).hex()
            return hmac.compare_digest(got, digest)
        except Exception:
            return False
    return hmac.compare_digest(password, stored)


def ensure_schema():
    with db() as conn:
        with conn.cursor() as cur:
            for stmt in SCHEMA:
                cur.execute(stmt)
            ensure_column(cur, "crm_contacts", "assigned_user_id", "assigned_user_id bigint unsigned null")
            ensure_column(cur, "crm_calls", "user_id", "user_id bigint unsigned null")
            user = cfg("CRM_BOOTSTRAP_USER")
            password = cfg("CRM_BOOTSTRAP_PASSWORD")
            if user and password:
                cur.execute("select count(*) as c from crm_users")
                if cur.fetchone()["c"] == 0:
                    cur.execute(
                        "insert into crm_users(username,password_hash,display_name,role) values(%s,%s,%s,'admin')",
                        (user, hash_password(password), cfg("CRM_BOOTSTRAP_DISPLAY", user)),
                    )


def ensure_column(cur, table, column, ddl):
    cur.execute(f"show columns from {table} like %s", (column,))
    if not cur.fetchone():
        cur.execute(f"alter table {table} add column {ddl}")


def query(sql, args=None):
    with db() as conn:
        with conn.cursor() as cur:
            cur.execute(sql, args or ())
            return cur.fetchall()


def one(sql, args=None):
    rows = query(sql, args)
    return rows[0] if rows else None


def execute(sql, args=None):
    with db() as conn:
        with conn.cursor() as cur:
            cur.execute(sql, args or ())
            return cur.lastrowid


class LoginFrame(ttk.Frame):
    def __init__(self, app):
        super().__init__(app.root, padding=32)
        self.app = app
        self.username = tk.StringVar(value=cfg("CRM_BOOTSTRAP_USER", ""))
        self.password = tk.StringVar()
        self.status = tk.StringVar(value="MySQL login")
        self.build()

    def build(self):
        self.place(relx=0.5, rely=0.5, anchor="center")
        ttk.Label(self, text=cfg("OFFICE_OS_BRAND", "Office-OS"), font=("DejaVu Sans", 28, "bold")).grid(row=0, column=0, columnspan=2, sticky="w")
        ttk.Label(self, text=cfg("OFFICE_OS_BYLINE", "By: Margo&GsmEsimDev"), foreground="#667085").grid(row=1, column=0, columnspan=2, sticky="w", pady=(0, 20))
        ttk.Label(self, text="Login").grid(row=2, column=0, sticky="w")
        ttk.Entry(self, textvariable=self.username, width=32).grid(row=2, column=1, sticky="ew", pady=5)
        ttk.Label(self, text="Password").grid(row=3, column=0, sticky="w")
        ent = ttk.Entry(self, textvariable=self.password, show="*", width=32)
        ent.grid(row=3, column=1, sticky="ew", pady=5)
        ttk.Button(self, text="Enter", command=self.login).grid(row=4, column=1, sticky="e", pady=(12, 0))
        ttk.Label(self, textvariable=self.status, foreground="#667085").grid(row=5, column=0, columnspan=2, sticky="w", pady=(14, 0))
        self.columnconfigure(1, weight=1)
        ent.bind("<Return>", lambda _e: self.login())
        ent.focus_set()

    def login(self):
        try:
            ensure_schema()
            user = one(
                "select * from crm_users where username=%s and active=1",
                (self.username.get().strip(),),
            )
            if not user or not verify_password(self.password.get(), user["password_hash"]):
                self.status.set("Wrong login or password")
                return
            execute("update crm_users set last_login=now() where id=%s", (user["id"],))
            self.destroy()
            self.app.show_main(user)
        except Exception as exc:
            self.status.set(f"MySQL error: {exc}")


class CRMApp:
    def __init__(self):
        self.root = tk.Tk()
        self.root.title(cfg("OFFICE_OS_BRAND", "Office-OS"))
        self.root.geometry("1120x720")
        self.root.minsize(900, 560)
        if cfg("CRM_DESKTOP_FULLSCREEN", "1") not in {"0", "false", "False"}:
            self.root.attributes("-fullscreen", True)
        self.root.bind("<Control-q>", lambda _e: self.root.destroy())
        self.user = None
        self.contact = None
        self.phone_window = None
        self.note_window = None
        self.call_proc = None
        self.search_var = tk.StringVar()
        self.status_var = tk.StringVar(value="")
        self.login = LoginFrame(self)

    def show_main(self, user):
        self.user = user
        root = self.root
        root.configure(background="#eef1f4")

        top = ttk.Frame(root, padding=(12, 10))
        top.pack(side="top", fill="x")
        ttk.Label(top, text=cfg("OFFICE_OS_BRAND", "Office-OS"), font=("DejaVu Sans", 18, "bold")).pack(side="left")
        ttk.Label(top, text=f"  {cfg('OFFICE_OS_BYLINE', 'By: Margo&GsmEsimDev')}", foreground="#667085").pack(side="left")
        ttk.Label(top, text=f"  {user['display_name'] or user['username']}", foreground="#667085").pack(side="left")
        ttk.Button(top, text="Exit", command=root.destroy).pack(side="right")

        body = ttk.Frame(root, padding=12)
        body.pack(fill="both", expand=True)
        body.columnconfigure(0, weight=0)
        body.columnconfigure(1, weight=1)
        body.rowconfigure(0, weight=1)

        left = ttk.Frame(body, width=330)
        left.grid(row=0, column=0, sticky="nsw", padx=(0, 10))
        ttk.Entry(left, textvariable=self.search_var, width=36).pack(fill="x", pady=(0, 8))
        self.search_var.trace_add("write", lambda *_: self.load_contacts())
        self.listbox = tk.Listbox(left, width=38, activestyle="dotbox", font=("DejaVu Sans", 11))
        self.listbox.pack(fill="both", expand=True)
        self.listbox.bind("<<ListboxSelect>>", self.on_select)

        card = ttk.Frame(body, padding=12)
        card.grid(row=0, column=1, sticky="nsew")
        card.columnconfigure(1, weight=1)
        self.vars = {
            "id": tk.StringVar(),
            "name": tk.StringVar(),
            "phone": tk.StringVar(),
            "company": tk.StringVar(),
            "status": tk.StringVar(value="new"),
        }
        labels = [("Name", "name"), ("Phone", "phone"), ("Company", "company"), ("Status", "status")]
        for row, (label, key) in enumerate(labels):
            ttk.Label(card, text=label).grid(row=row, column=0, sticky="w", pady=5)
            if key == "status":
                widget = ttk.Combobox(card, textvariable=self.vars[key], values=("new", "call", "no_answer", "deal", "bad"), state="readonly")
            else:
                widget = ttk.Entry(card, textvariable=self.vars[key])
            widget.grid(row=row, column=1, sticky="ew", pady=5)
        ttk.Label(card, text="Notes").grid(row=4, column=0, sticky="nw", pady=5)
        self.notes = tk.Text(card, height=14, wrap="word", font=("DejaVu Sans", 11))
        self.notes.grid(row=4, column=1, sticky="nsew", pady=5)
        card.rowconfigure(4, weight=1)

        buttons = ttk.Frame(card)
        buttons.grid(row=5, column=1, sticky="ew", pady=(12, 0))
        ttk.Button(buttons, text="Save", command=self.save_contact).pack(side="left")
        ttk.Button(buttons, text="New", command=self.new_contact).pack(side="left", padx=6)
        ttk.Button(buttons, text="Call window", command=self.open_phone_popup).pack(side="left", padx=6)
        ttk.Button(buttons, text="Note window", command=self.open_note_popup).pack(side="left", padx=6)
        ttk.Button(buttons, text="Refresh", command=self.load_contacts).pack(side="right")

        ttk.Label(root, textvariable=self.status_var, foreground="#667085", padding=(12, 6)).pack(side="bottom", fill="x")
        self.contacts = []
        self.load_contacts()

    def load_contacts(self):
        try:
            q = self.search_var.get().strip()
            like = f"%{q}%"
            self.contacts = query(
                "select id,name,phone,company,status,notes,updated_at from crm_contacts "
                "where %s='' or name like %s or phone like %s or company like %s or notes like %s "
                "order by updated_at desc limit 300",
                (q, like, like, like, like),
            )
            self.listbox.delete(0, "end")
            for c in self.contacts:
                title = c["name"] or c["phone"] or f"#{c['id']}"
                self.listbox.insert("end", f"{title}  {c['phone']}  [{c['status']}]")
            self.status_var.set(f"Loaded {len(self.contacts)} contacts")
        except Exception as exc:
            self.status_var.set(f"MySQL error: {exc}")

    def on_select(self, _event=None):
        sel = self.listbox.curselection()
        if not sel:
            return
        self.contact = self.contacts[sel[0]]
        self.fill_card(self.contact)
        self.open_phone_popup()
        self.open_note_popup()

    def fill_card(self, c):
        for key in ("id", "name", "phone", "company", "status"):
            self.vars[key].set(str(c.get(key) or ""))
        self.notes.delete("1.0", "end")
        self.notes.insert("1.0", c.get("notes") or "")

    def collect_card(self):
        return {
            "id": self.vars["id"].get().strip(),
            "name": self.vars["name"].get().strip(),
            "phone": self.vars["phone"].get().strip(),
            "company": self.vars["company"].get().strip(),
            "status": self.vars["status"].get().strip() or "new",
            "notes": self.notes.get("1.0", "end").strip(),
        }

    def save_contact(self):
        data = self.collect_card()
        try:
            if data["id"]:
                execute(
                    "update crm_contacts set name=%s,phone=%s,company=%s,status=%s,notes=%s where id=%s",
                    (data["name"], data["phone"], data["company"], data["status"], data["notes"], data["id"]),
                )
                cid = data["id"]
            else:
                cid = execute(
                    "insert into crm_contacts(name,phone,company,status,notes,assigned_user_id) values(%s,%s,%s,%s,%s,%s)",
                    (data["name"], data["phone"], data["company"], data["status"], data["notes"], self.user["id"]),
                )
            self.contact = one("select id,name,phone,company,status,notes,updated_at from crm_contacts where id=%s", (cid,))
            self.fill_card(self.contact)
            self.load_contacts()
            self.status_var.set("Saved")
        except Exception as exc:
            self.status_var.set(f"Save error: {exc}")

    def new_contact(self):
        self.contact = None
        for key in ("id", "name", "phone", "company"):
            self.vars[key].set("")
        self.vars["status"].set("new")
        self.notes.delete("1.0", "end")

    def open_phone_popup(self):
        if self.phone_window and self.phone_window.winfo_exists():
            self.phone_window.refresh()
            self.phone_window.lift()
            return
        self.phone_window = PhonePopup(self)

    def open_note_popup(self):
        if self.note_window and self.note_window.winfo_exists():
            self.note_window.refresh()
            self.note_window.lift()
            return
        self.note_window = NotePopup(self)

    def log_call(self, phone, status, note):
        cid = self.vars["id"].get().strip() or None
        execute(
            "insert into crm_calls(contact_id,user_id,phone,direction,status,note) values(%s,%s,%s,'out',%s,%s)",
            (cid, self.user["id"], phone, status, note),
        )

    def run(self):
        self.root.mainloop()


class PhonePopup(tk.Toplevel):
    def __init__(self, app):
        super().__init__(app.root)
        self.app = app
        self.phone = tk.StringVar()
        self.state = tk.StringVar(value="")
        self.title("Softphone")
        self.geometry("360x190+760+90")
        self.attributes("-topmost", True)
        ttk.Label(self, text="Softphone", font=("DejaVu Sans", 16, "bold")).pack(anchor="w", padx=14, pady=(12, 6))
        ttk.Entry(self, textvariable=self.phone, font=("DejaVu Sans", 14)).pack(fill="x", padx=14, pady=6)
        row = ttk.Frame(self)
        row.pack(fill="x", padx=14, pady=8)
        ttk.Button(row, text="Dial", command=self.dial).pack(side="left")
        ttk.Button(row, text="Hang", command=self.hang).pack(side="left", padx=8)
        ttk.Button(row, text="Close", command=self.destroy).pack(side="right")
        ttk.Label(self, textvariable=self.state, foreground="#667085").pack(anchor="w", padx=14, pady=4)
        self.refresh()

    def refresh(self):
        self.phone.set(self.app.vars["phone"].get())

    def dial(self):
        phone = self.phone.get().strip()
        if not phone:
            self.state.set("No phone")
            return
        try:
            self.app.log_call(phone, "dial", "dial from desktop popup")
            self.app.call_proc = subprocess.Popen(["/usr/local/bin/crm-softphone", "dial", phone])
            self.state.set("Dialing")
        except Exception as exc:
            self.state.set(f"Dial error: {exc}")

    def hang(self):
        proc = self.app.call_proc
        if proc and proc.poll() is None:
            proc.terminate()
            self.state.set("Stopped")
        else:
            self.state.set("No active call")


class NotePopup(tk.Toplevel):
    def __init__(self, app):
        super().__init__(app.root)
        self.app = app
        self.title("Client note")
        self.geometry("460x360+720+300")
        self.attributes("-topmost", True)
        ttk.Label(self, text="Client note", font=("DejaVu Sans", 16, "bold")).pack(anchor="w", padx=14, pady=(12, 6))
        self.text = tk.Text(self, wrap="word", font=("DejaVu Sans", 12))
        self.text.pack(fill="both", expand=True, padx=14, pady=6)
        row = ttk.Frame(self)
        row.pack(fill="x", padx=14, pady=(4, 12))
        ttk.Button(row, text="Save note", command=self.save).pack(side="left")
        ttk.Button(row, text="Close", command=self.destroy).pack(side="right")
        self.refresh()

    def refresh(self):
        self.text.delete("1.0", "end")
        self.text.insert("1.0", self.app.notes.get("1.0", "end").strip())

    def save(self):
        self.app.notes.delete("1.0", "end")
        self.app.notes.insert("1.0", self.text.get("1.0", "end").strip())
        self.app.save_contact()


if __name__ == "__main__":
    CRMApp().run()
