import os
from datetime import date, datetime, timedelta, timezone
from decimal import Decimal, InvalidOperation

from cryptography.fernet import Fernet, InvalidToken
from flask import Flask, abort, flash, redirect, render_template, request, url_for
from flask_login import (LoginManager, UserMixin, current_user, login_required,
                         login_user, logout_user)
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
from flask_wtf.csrf import CSRFProtect
from sqlalchemy import or_
from werkzeug.security import check_password_hash, generate_password_hash

app = Flask(__name__)
app.config.update(
    SECRET_KEY=os.environ.get("APP_SECRET", "dev-only-change-this-secret"),
    SQLALCHEMY_DATABASE_URI=os.environ.get("DATABASE_URL", "sqlite:///service_desk.db"),
    SQLALCHEMY_TRACK_MODIFICATIONS=False,
    SESSION_COOKIE_HTTPONLY=True,
    SESSION_COOKIE_SAMESITE="Lax",
    SESSION_COOKIE_SECURE=os.environ.get("COOKIE_SECURE", "false").lower() == "true",
    WTF_CSRF_TIME_LIMIT=None,
)
db = SQLAlchemy(app)
Migrate(app, db, render_as_batch=True)
csrf = CSRFProtect(app)
login_manager = LoginManager(app)
login_manager.login_view = "login"
login_manager.login_message = "Sign in to continue."


def credential_cipher():
    key = os.environ.get("CREDENTIAL_KEY", "")
    try:
        return Fernet(key.encode())
    except (ValueError, TypeError):
        raise RuntimeError("Set CREDENTIAL_KEY to a valid Fernet key before starting the app.")


if app.config["SECRET_KEY"] == "dev-only-change-this-secret":
    raise RuntimeError("Set APP_SECRET to a long random value before starting the app.")
credential_cipher()


def utcnow():
    return datetime.now(timezone.utc).replace(tzinfo=None)


class Staff(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), nullable=False)
    email = db.Column(db.String(180), unique=True, nullable=False, index=True)
    password_hash = db.Column(db.String(255), nullable=False)
    role = db.Column(db.String(20), default="Staff", nullable=False)


class Customer(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(160), nullable=False, index=True)
    company = db.Column(db.String(160), default="")
    email = db.Column(db.String(180), default="")
    phone = db.Column(db.String(80), default="")
    address = db.Column(db.Text, default="")
    notes = db.Column(db.Text, default="")
    credentials_encrypted = db.Column(db.Text, default="")
    created_at = db.Column(db.DateTime, default=utcnow, nullable=False)
    tickets = db.relationship("Ticket", backref="customer", lazy=True)
    subscriptions = db.relationship("Subscription", backref="customer", lazy=True)


class Ticket(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    customer_id = db.Column(db.Integer, db.ForeignKey("customer.id"), nullable=False)
    title = db.Column(db.String(200), nullable=False)
    details = db.Column(db.Text, default="")
    category = db.Column(db.String(80), default="Computer repair")
    status = db.Column(db.String(30), default="Open", nullable=False)
    priority = db.Column(db.String(20), default="Normal", nullable=False)
    assignee = db.Column(db.String(120), default="")
    created_at = db.Column(db.DateTime, default=utcnow, nullable=False)
    entries = db.relationship("TimeEntry", backref="ticket", lazy=True, cascade="all, delete-orphan")

    @property
    def minutes_logged(self):
        total = 0
        for entry in self.entries:
            total += entry.minutes if entry.ended_at else int((utcnow() - entry.started_at).total_seconds() // 60)
        return total

    @property
    def active_entry(self):
        return next((e for e in self.entries if e.ended_at is None), None)


class TimeEntry(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    ticket_id = db.Column(db.Integer, db.ForeignKey("ticket.id"), nullable=False)
    staff_id = db.Column(db.Integer, db.ForeignKey("staff.id"), nullable=False)
    staff_name = db.Column(db.String(100), nullable=False)
    started_at = db.Column(db.DateTime, default=utcnow, nullable=False)
    ended_at = db.Column(db.DateTime)
    minutes = db.Column(db.Integer, default=0, nullable=False)


class Subscription(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    customer_id = db.Column(db.Integer, db.ForeignKey("customer.id"), nullable=False)
    name = db.Column(db.String(180), nullable=False)
    amount_cents = db.Column(db.Integer, nullable=False)
    interval = db.Column(db.String(20), default="Monthly", nullable=False)
    next_invoice = db.Column(db.Date, nullable=False)
    status = db.Column(db.String(20), default="Active", nullable=False)
    notes = db.Column(db.Text, default="")


@login_manager.user_loader
def load_user(user_id):
    return db.session.get(Staff, int(user_id))


@app.context_processor
def template_helpers():
    return {"now": datetime.now}


def date_value(raw):
    return datetime.strptime(raw, "%Y-%m-%d").date()


def customer_form(customer=None):
    if request.method == "POST":
        customer.name = request.form.get("name", "").strip()
        customer.company = request.form.get("company", "").strip()
        customer.email = request.form.get("email", "").strip()
        customer.phone = request.form.get("phone", "").strip()
        customer.address = request.form.get("address", "").strip()
        customer.notes = request.form.get("notes", "").strip()
        secret = request.form.get("credentials", "")
        if secret:
            customer.credentials_encrypted = credential_cipher().encrypt(secret.encode()).decode()
        elif request.form.get("clear_credentials"):
            customer.credentials_encrypted = ""
        if not customer.name:
            flash("Customer name is required.", "error")
            return render_template("customer_form.html", customer=customer)
        db.session.add(customer)
        db.session.commit()
        flash("Customer saved.", "success")
        return redirect(url_for("customer_detail", customer_id=customer.id))
    return render_template("customer_form.html", customer=customer)


@app.get("/health")
def health():
    return {"status": "ok"}


@app.route("/setup", methods=["GET", "POST"])
def setup():
    if Staff.query.first():
        return redirect(url_for("login"))
    if request.method == "POST":
        name = request.form.get("name", "").strip()
        email = request.form.get("email", "").strip().lower()
        password = request.form.get("password", "")
        if not name or "@" not in email or len(password) < 12:
            flash("Enter your name, a valid email, and a password with at least 12 characters.", "error")
        else:
            staff = Staff(name=name, email=email, password_hash=generate_password_hash(password), role="Owner")
            db.session.add(staff)
            db.session.commit()
            login_user(staff)
            return redirect(url_for("dashboard"))
    return render_template("setup.html")


@app.route("/login", methods=["GET", "POST"])
def login():
    if not Staff.query.first():
        return redirect(url_for("setup"))
    if current_user.is_authenticated:
        return redirect(url_for("dashboard"))
    if request.method == "POST":
        email = request.form.get("email", "").strip().lower()
        staff = Staff.query.filter_by(email=email).first()
        if staff and check_password_hash(staff.password_hash, request.form.get("password", "")):
            login_user(staff)
            return redirect(url_for("dashboard"))
        flash("Email or password was not recognized.", "error")
    return render_template("login.html")


@app.post("/logout")
@login_required
def logout():
    logout_user()
    return redirect(url_for("login"))


@app.route("/team", methods=["GET", "POST"])
@login_required
def team():
    if current_user.role != "Owner":
        abort(403)
    if request.method == "POST":
        name = request.form.get("name", "").strip()
        email = request.form.get("email", "").strip().lower()
        password = request.form.get("password", "")
        if not name or "@" not in email or len(password) < 12:
            flash("Enter a name, a valid email, and a password with at least 12 characters.", "error")
        elif Staff.query.filter_by(email=email).first():
            flash("That email address already has an account.", "error")
        else:
            db.session.add(Staff(name=name, email=email, password_hash=generate_password_hash(password), role="Staff"))
            db.session.commit()
            flash("Team member added.", "success")
            return redirect(url_for("team"))
    return render_template("team.html", members=Staff.query.order_by(Staff.name).all())


@app.route("/account/password", methods=["GET", "POST"])
@login_required
def account_password():
    if request.method == "POST":
        current_password = request.form.get("current_password", "")
        new_password = request.form.get("new_password", "")
        confirm_password = request.form.get("confirm_password", "")
        if not check_password_hash(current_user.password_hash, current_password):
            flash("Your current password was not recognized.", "error")
        elif len(new_password) < 12:
            flash("Choose a password with at least 12 characters.", "error")
        elif new_password != confirm_password:
            flash("The new passwords do not match.", "error")
        else:
            current_user.password_hash = generate_password_hash(new_password)
            db.session.commit()
            flash("Your password has been changed.", "success")
            return redirect(url_for("account_password"))
    return render_template("account_password.html")


@app.get("/")
@login_required
def dashboard():
    today = date.today()
    soon = today + timedelta(days=14)
    subscriptions = (Subscription.query.join(Customer).filter(
        Subscription.status == "Active", Subscription.next_invoice <= soon
    ).order_by(Subscription.next_invoice).all())
    running = TimeEntry.query.filter_by(ended_at=None).count()
    return render_template("dashboard.html", customer_count=Customer.query.count(),
                           open_count=Ticket.query.filter(Ticket.status != "Closed").count(),
                           running=running, due_count=sum(s.next_invoice <= today for s in subscriptions),
                           subscriptions=subscriptions, today=today)


@app.get("/customers")
@login_required
def customers():
    q = request.args.get("q", "").strip()
    query = Customer.query
    if q:
        like = f"%{q}%"
        query = query.filter(or_(Customer.name.ilike(like), Customer.company.ilike(like), Customer.email.ilike(like)))
    return render_template("customers.html", customers=query.order_by(Customer.name).all(), q=q)


@app.route("/customers/new", methods=["GET", "POST"])
@login_required
def customer_new():
    return customer_form(Customer())


@app.route("/customers/<int:customer_id>/edit", methods=["GET", "POST"])
@login_required
def customer_edit(customer_id):
    return customer_form(db.get_or_404(Customer, customer_id))


@app.get("/customers/<int:customer_id>")
@login_required
def customer_detail(customer_id):
    customer = db.get_or_404(Customer, customer_id)
    return render_template("customer_detail.html", customer=customer, tickets=Ticket.query.filter_by(customer_id=customer.id).order_by(Ticket.created_at.desc()).all(), subscriptions=Subscription.query.filter_by(customer_id=customer.id).order_by(Subscription.next_invoice).all())


@app.post("/customers/<int:customer_id>/credentials")
@login_required
def reveal_credentials(customer_id):
    customer = db.get_or_404(Customer, customer_id)
    try:
        revealed = credential_cipher().decrypt(customer.credentials_encrypted.encode()).decode() if customer.credentials_encrypted else ""
    except InvalidToken:
        abort(500, "Could not decrypt credentials. Check the configured credential key.")
    return render_template("credentials.html", customer=customer, revealed=revealed)


@app.get("/tickets")
@login_required
def tickets():
    status = request.args.get("status", "")
    query = Ticket.query.join(Customer)
    if status:
        query = query.filter(Ticket.status == status)
    return render_template("tickets.html", tickets=query.order_by(Ticket.created_at.desc()).all(), status=status)


@app.route("/tickets/new", methods=["GET", "POST"])
@login_required
def ticket_new():
    customer_id = request.args.get("customer", type=int)
    if request.method == "POST":
        ticket = Ticket(customer_id=request.form.get("customer_id", type=int), title=request.form.get("title", "").strip(), details=request.form.get("details", "").strip(), category=request.form.get("category", "Computer repair"), priority=request.form.get("priority", "Normal"), assignee=request.form.get("assignee", "").strip())
        if not ticket.title or not db.session.get(Customer, ticket.customer_id):
            flash("Choose a customer and enter a ticket title.", "error")
        else:
            db.session.add(ticket)
            db.session.commit()
            flash("Ticket created.", "success")
            return redirect(url_for("ticket_detail", ticket_id=ticket.id))
    return render_template("ticket_form.html", customers=Customer.query.order_by(Customer.name).all(), members=Staff.query.order_by(Staff.name).all(), customer_id=customer_id)


@app.get("/tickets/<int:ticket_id>")
@login_required
def ticket_detail(ticket_id):
    ticket = db.get_or_404(Ticket, ticket_id)
    return render_template("ticket_detail.html", ticket=ticket)


@app.post("/tickets/<int:ticket_id>/timer/start")
@login_required
def timer_start(ticket_id):
    ticket = db.get_or_404(Ticket, ticket_id)
    if TimeEntry.query.filter_by(staff_id=current_user.id, ended_at=None).first():
        flash("Stop your active timer before starting another one.", "error")
    elif ticket.status == "Closed":
        flash("Reopen the ticket before tracking time.", "error")
    else:
        ticket.status = "In progress"
        db.session.add(TimeEntry(ticket_id=ticket.id, staff_id=current_user.id, staff_name=current_user.name))
        db.session.commit()
        flash("Timer started.", "success")
    return redirect(url_for("ticket_detail", ticket_id=ticket.id))


@app.post("/tickets/<int:ticket_id>/timer/stop")
@login_required
def timer_stop(ticket_id):
    ticket = db.get_or_404(Ticket, ticket_id)
    entry = TimeEntry.query.filter_by(ticket_id=ticket.id, staff_id=current_user.id, ended_at=None).first()
    if entry:
        entry.ended_at = utcnow()
        entry.minutes = max(1, int((entry.ended_at - entry.started_at).total_seconds() // 60))
        db.session.commit()
        flash(f"Time saved: {entry.minutes} minute(s).", "success")
    return redirect(url_for("ticket_detail", ticket_id=ticket.id))


@app.post("/tickets/<int:ticket_id>/status")
@login_required
def ticket_status(ticket_id):
    ticket = db.get_or_404(Ticket, ticket_id)
    status = request.form.get("status")
    if status in {"Open", "In progress", "Waiting", "Closed"}:
        ticket.status = status
        db.session.commit()
    return redirect(url_for("ticket_detail", ticket_id=ticket.id))


@app.get("/subscriptions")
@login_required
def subscriptions():
    return render_template("subscriptions.html", subscriptions=Subscription.query.join(Customer).order_by(Subscription.next_invoice).all())


@app.route("/subscriptions/new", methods=["GET", "POST"])
@login_required
def subscription_new():
    customer_id = request.args.get("customer", type=int)
    if request.method == "POST":
        try:
            amount = Decimal(request.form.get("amount", "0")).quantize(Decimal("0.01"))
            amount_cents = int(amount * 100)
            due = date_value(request.form.get("next_invoice", ""))
        except (InvalidOperation, ValueError):
            flash("Enter a valid price and next invoice date.", "error")
        else:
            sub = Subscription(customer_id=request.form.get("customer_id", type=int), name=request.form.get("name", "").strip(), amount_cents=amount_cents, interval=request.form.get("interval", "Monthly"), next_invoice=due, notes=request.form.get("notes", "").strip())
            if not sub.name or amount_cents < 0 or not db.session.get(Customer, sub.customer_id):
                flash("Choose a customer, enter a service name, and use a nonnegative price.", "error")
            else:
                db.session.add(sub)
                db.session.commit()
                flash("Subscription added.", "success")
                return redirect(url_for("customer_detail", customer_id=sub.customer_id))
    return render_template("subscription_form.html", customers=Customer.query.order_by(Customer.name).all(), customer_id=customer_id)


@app.post("/subscriptions/<int:subscription_id>/invoiced")
@login_required
def subscription_invoiced(subscription_id):
    sub = db.get_or_404(Subscription, subscription_id)
    if sub.interval == "Monthly":
        month = sub.next_invoice.month + 1
        year = sub.next_invoice.year + (month - 1) // 12
        month = (month - 1) % 12 + 1
        day = min(sub.next_invoice.day, [31, 29 if year % 4 == 0 and (year % 100 != 0 or year % 400 == 0) else 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1])
        sub.next_invoice = date(year, month, day)
    elif sub.interval == "Quarterly":
        for _ in range(3):
            month = sub.next_invoice.month + 1
            year = sub.next_invoice.year + (month - 1) // 12
            month = (month - 1) % 12 + 1
            day = min(sub.next_invoice.day, [31, 29 if year % 4 == 0 and (year % 100 != 0 or year % 400 == 0) else 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1])
            sub.next_invoice = date(year, month, day)
    elif sub.interval == "Yearly":
        year = sub.next_invoice.year + 1
        month = sub.next_invoice.month
        day = min(sub.next_invoice.day, [31, 29 if year % 4 == 0 and (year % 100 != 0 or year % 400 == 0) else 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1])
        sub.next_invoice = date(year, month, day)
    elif sub.interval == "One-time":
        sub.status = "Cancelled"
    db.session.commit()
    flash("Invoice marked as sent and next invoice date advanced.", "success")
    return redirect(url_for("subscriptions"))


@app.post("/subscriptions/<int:subscription_id>/status")
@login_required
def subscription_status(subscription_id):
    sub = db.get_or_404(Subscription, subscription_id)
    status = request.form.get("status")
    if status in {"Active", "Paused", "Cancelled"}:
        sub.status = status
        db.session.commit()
    return redirect(url_for("subscriptions"))


@app.template_filter("money")
def money(cents):
    return f"${cents / 100:,.2f}"


@app.template_filter("duration")
def duration(minutes):
    return f"{minutes // 60}h {minutes % 60:02d}m" if minutes >= 60 else f"{minutes}m"


@app.template_filter("elapsed")
def elapsed(started):
    return (utcnow() - started).total_seconds() // 60
