import os
import secrets
from datetime import datetime
from pathlib import Path

import bleach
from flask import Flask, abort, flash, jsonify, redirect, render_template, request, url_for
from flask_login import LoginManager, UserMixin, current_user, login_required, login_user, logout_user
from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import check_password_hash, generate_password_hash
from werkzeug.utils import secure_filename


BASE_DIR = Path(__file__).resolve().parent
INSTANCE_DIR = BASE_DIR / "instance"
UPLOAD_DIR = BASE_DIR / "static" / "uploads"
ALLOWED_IMAGE_EXTENSIONS = {"png", "jpg", "jpeg", "gif", "webp"}

db = SQLAlchemy()
login_manager = LoginManager()
login_manager.login_view = "login"


def create_app():
    app = Flask(__name__, instance_path=str(INSTANCE_DIR), instance_relative_config=True)
    INSTANCE_DIR.mkdir(exist_ok=True)
    UPLOAD_DIR.mkdir(parents=True, exist_ok=True)

    secret_file = INSTANCE_DIR / "secret_key.txt"
    if not secret_file.exists():
        secret_file.write_text(secrets.token_hex(32), encoding="utf-8")

    app.config.update(
        SECRET_KEY=secret_file.read_text(encoding="utf-8").strip(),
        SQLALCHEMY_DATABASE_URI=f"sqlite:///{INSTANCE_DIR / 'site.db'}",
        SQLALCHEMY_TRACK_MODIFICATIONS=False,
        MAX_CONTENT_LENGTH=8 * 1024 * 1024,
        UPLOAD_FOLDER=str(UPLOAD_DIR),
    )

    db.init_app(app)
    login_manager.init_app(app)

    register_routes(app)
    with app.app_context():
        init_db()
    return app


class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    password_hash = db.Column(db.String(255), nullable=False)
    role = db.Column(db.String(20), nullable=False, default="user")
    must_change_password = db.Column(db.Boolean, default=False, nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)

    def set_password(self, password):
        self.password_hash = generate_password_hash(password)

    def check_password(self, password):
        return check_password_hash(self.password_hash, password)

    @property
    def is_admin(self):
        return self.role == "admin"

    @property
    def is_editor(self):
        return self.role == "editor"

    @property
    def can_write_posts(self):
        return self.role in {"admin", "editor"}


class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), nullable=False)
    body = db.Column(db.Text, nullable=False)
    author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
    edited_by_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=True)
    created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
    updated_at = db.Column(db.DateTime, nullable=True)
    author = db.relationship("User", foreign_keys=[author_id], backref="posts")
    edited_by = db.relationship("User", foreign_keys=[edited_by_id])


class Comment(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    post_id = db.Column(db.Integer, db.ForeignKey("post.id"), nullable=False)
    user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=True)
    nickname = db.Column(db.String(80), nullable=False)
    body = db.Column(db.Text, nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
    post = db.relationship("Post", backref=db.backref("comments", cascade="all, delete-orphan", lazy=True))
    user = db.relationship("User")


class PageContent(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    slug = db.Column(db.String(40), unique=True, nullable=False)
    title = db.Column(db.String(120), nullable=False)
    body = db.Column(db.Text, default="", nullable=False)
    updated_at = db.Column(db.DateTime, nullable=True)
    updated_by_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=True)
    updated_by = db.relationship("User")


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


def init_db():
    db.create_all()
    for slug, title in (("offer", "Offer"), ("ports", "Ports"), ("server_spec", "Server Spec")):
        if not PageContent.query.filter_by(slug=slug).first():
            db.session.add(PageContent(slug=slug, title=title, body=""))

    if not User.query.filter_by(username="Gryzak").first():
        password = secrets.token_urlsafe(14)
        admin = User(username="Gryzak", role="admin", must_change_password=True)
        admin.set_password(password)
        db.session.add(admin)
        (INSTANCE_DIR / "initial_admin_password.txt").write_text(
            f"Login: Gryzak\nHaslo: {password}\nZmien je po pierwszym logowaniu.\n",
            encoding="utf-8",
        )
    db.session.commit()


def sanitize_html(html):
    allowed_tags = set(bleach.sanitizer.ALLOWED_TAGS).union({
        "p", "br", "span", "div", "h1", "h2", "h3", "h4", "ul", "ol", "li", "img", "iframe",
        "blockquote", "strong", "em", "u", "s", "pre", "code",
    })
    allowed_attrs = {
        "*": ["class", "style"],
        "a": ["href", "title", "target", "rel"],
        "img": ["src", "alt", "title", "width", "height"],
        "iframe": ["src", "width", "height", "allow", "allowfullscreen", "frameborder"],
    }
    return bleach.clean(html or "", tags=allowed_tags, attributes=allowed_attrs, protocols=["http", "https", "mailto"], strip=True)


def require_admin():
    if not current_user.is_authenticated or not current_user.is_admin:
        abort(403)


def can_edit_post(post):
    return current_user.is_authenticated and (current_user.is_admin or (current_user.is_editor and post.author_id == current_user.id))


def valid_image(file_storage):
    filename = secure_filename(file_storage.filename or "")
    ext = filename.rsplit(".", 1)[-1].lower() if "." in filename else ""
    if ext not in ALLOWED_IMAGE_EXTENSIONS:
        return None
    file_storage.stream.seek(0)
    header = file_storage.stream.read(32)
    file_storage.stream.seek(0)
    signatures = {
        "png": header.startswith(b"\x89PNG\r\n\x1a\n"),
        "jpg": header.startswith(b"\xff\xd8\xff"),
        "jpeg": header.startswith(b"\xff\xd8\xff"),
        "gif": header.startswith((b"GIF87a", b"GIF89a")),
        "webp": header.startswith(b"RIFF") and header[8:12] == b"WEBP",
    }
    if not signatures.get(ext):
        return None
    return ext


def register_routes(app):
    @app.template_filter("dt")
    def format_dt(value):
        return value.strftime("%Y-%m-%d %H:%M:%S") if value else ""

    @app.route("/")
    def home():
        page = request.args.get("page", 1, type=int)
        posts = Post.query.order_by(Post.created_at.desc()).paginate(page=page, per_page=5, error_out=False)
        return render_template("home.html", posts=posts, can_edit_post=can_edit_post)

    @app.route("/offer")
    def offer():
        page = PageContent.query.filter_by(slug="offer").first_or_404()
        return render_template("page.html", page=page)

    @app.route("/ports")
    @login_required
    def ports():
        page = PageContent.query.filter_by(slug="ports").first_or_404()
        return render_template("page.html", page=page)

    @app.route("/server-spec")
    def server_spec():
        page = PageContent.query.filter_by(slug="server_spec").first_or_404()
        return render_template("page.html", page=page)

    @app.route("/login", methods=["GET", "POST"])
    def login():
        if request.method == "POST":
            user = User.query.filter_by(username=request.form.get("username", "").strip()).first()
            if user and user.check_password(request.form.get("password", "")):
                login_user(user)
                if user.must_change_password:
                    return redirect(url_for("change_password"))
                return redirect(url_for("home"))
            flash("Nieprawidlowy login lub haslo.", "error")
        return render_template("login.html")

    @app.route("/logout")
    @login_required
    def logout():
        logout_user()
        return redirect(url_for("home"))

    @app.route("/change-password", methods=["GET", "POST"])
    @login_required
    def change_password():
        if request.method == "POST":
            password = request.form.get("password", "")
            if len(password) < 10:
                flash("Haslo musi miec minimum 10 znakow.", "error")
            elif password != request.form.get("password_confirm", ""):
                flash("Hasla nie sa takie same.", "error")
            else:
                current_user.set_password(password)
                current_user.must_change_password = False
                db.session.commit()
                flash("Haslo zostalo zmienione.", "success")
                return redirect(url_for("home"))
        return render_template("change_password.html")

    @app.route("/post/new", methods=["GET", "POST"])
    @login_required
    def post_new():
        if not current_user.can_write_posts:
            abort(403)
        if request.method == "POST":
            title = request.form.get("title", "").strip()
            body = sanitize_html(request.form.get("body", ""))
            if not title or not body:
                flash("Tytul i tresc sa wymagane.", "error")
            else:
                db.session.add(Post(title=title, body=body, author=current_user))
                db.session.commit()
                return redirect(url_for("home"))
        return render_template("post_form.html", post=None)

    @app.route("/post/<int:post_id>/edit", methods=["GET", "POST"])
    @login_required
    def post_edit(post_id):
        post = Post.query.get_or_404(post_id)
        if not can_edit_post(post):
            abort(403)
        if request.method == "POST":
            post.title = request.form.get("title", "").strip()
            post.body = sanitize_html(request.form.get("body", ""))
            post.updated_at = datetime.utcnow()
            post.edited_by = current_user
            db.session.commit()
            return redirect(url_for("home"))
        return render_template("post_form.html", post=post)

    @app.route("/post/<int:post_id>/delete", methods=["POST"])
    @login_required
    def post_delete(post_id):
        require_admin()
        post = Post.query.get_or_404(post_id)
        db.session.delete(post)
        db.session.commit()
        return redirect(url_for("home"))

    @app.route("/post/<int:post_id>/comment", methods=["POST"])
    def comment_add(post_id):
        post = Post.query.get_or_404(post_id)
        body = request.form.get("body", "").strip()
        nickname = current_user.username if current_user.is_authenticated else request.form.get("nickname", "").strip()
        if not nickname:
            nickname = "Anonim"
        if body:
            db.session.add(Comment(post=post, user=current_user if current_user.is_authenticated else None, nickname=nickname[:80], body=body[:2000]))
            db.session.commit()
        return redirect(url_for("home") + f"#post-{post.id}")

    @app.route("/comment/<int:comment_id>/delete", methods=["POST"])
    @login_required
    def comment_delete(comment_id):
        require_admin()
        comment = Comment.query.get_or_404(comment_id)
        db.session.delete(comment)
        db.session.commit()
        return redirect(url_for("home"))

    @app.route("/admin", methods=["GET", "POST"])
    @login_required
    def admin():
        require_admin()
        if request.method == "POST":
            username = request.form.get("username", "").strip()
            password = request.form.get("password", "")
            role = request.form.get("role", "user")
            if role == "editor" and current_user.username != "Gryzak":
                abort(403)
            if role not in {"user", "editor"}:
                role = "user"
            if username and len(password) >= 8 and not User.query.filter_by(username=username).first():
                user = User(username=username, role=role, must_change_password=True)
                user.set_password(password)
                db.session.add(user)
                db.session.commit()
                flash("Uzytkownik zostal dodany.", "success")
            else:
                flash("Sprawdz login, haslo lub czy konto juz istnieje.", "error")
        users = User.query.order_by(User.username.asc()).all()
        return render_template("admin.html", users=users)

    @app.route("/admin/user/<int:user_id>", methods=["POST"])
    @login_required
    def admin_user_update(user_id):
        require_admin()
        user = User.query.get_or_404(user_id)
        if user.username == "Gryzak":
            flash("Konto Gryzak jest chronione.", "error")
            return redirect(url_for("admin"))
        action = request.form.get("action")
        if action == "delete":
            db.session.delete(user)
        elif action == "role":
            role = request.form.get("role", "user")
            if role == "editor" and current_user.username != "Gryzak":
                abort(403)
            if role in {"user", "editor"}:
                user.role = role
        elif action == "password":
            password = request.form.get("password", "")
            if len(password) >= 8:
                user.set_password(password)
                user.must_change_password = True
        db.session.commit()
        return redirect(url_for("admin"))

    @app.route("/admin/page/<slug>", methods=["GET", "POST"])
    @login_required
    def page_edit(slug):
        require_admin()
        page = PageContent.query.filter_by(slug=slug).first_or_404()
        if request.method == "POST":
            page.body = sanitize_html(request.form.get("body", ""))
            page.updated_at = datetime.utcnow()
            page.updated_by = current_user
            db.session.commit()
            endpoint = "server_spec" if slug == "server_spec" else slug
            return redirect(url_for(endpoint))
        return render_template("page_form.html", page=page)

    @app.route("/upload-image", methods=["POST"])
    @login_required
    def upload_image():
        if not current_user.can_write_posts and not current_user.is_admin:
            abort(403)
        uploaded = request.files.get("file")
        if not uploaded:
            return jsonify({"error": "Brak pliku"}), 400
        ext = valid_image(uploaded)
        if not ext:
            return jsonify({"error": "Dozwolone sa tylko prawdziwe pliki obrazow"}), 400
        filename = f"{secrets.token_hex(12)}.{ext}"
        uploaded.save(UPLOAD_DIR / filename)
        return jsonify({"location": url_for("static", filename=f"uploads/{filename}")})


app = create_app()


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000, debug=True)
