from sqlalchemy import text
from sqlalchemy.orm import selectinload
from sqlalchemy.orm.attributes import set_committed_value

from files.__main__ import app, limiter
from files.classes import *
from files.classes.visstate import StateMod
from files.helpers.alerts import *
from files.helpers.comments import comment_on_publish
from files.helpers.config.const import *
from files.helpers.media import process_image
from files.helpers.wrappers import *
from files.routes.importstar import *

from datetime import datetime, timezone

# Collect just the comment ids the context view actually renders: the focused
# comment's subtree (down to the render depth limit, +1 level so the boundary
# "more comments" button still appears) and its ancestor chain (up `context`
# levels, +1 so top_comment.parent_comment stays wired). This replaces loading
# the entire top-comment thread and discarding most of it.
_CONTEXT_COMMENT_IDS_SQL = text("""
WITH RECURSIVE subtree(id, depth) AS (
		SELECT id, 0 FROM comments WHERE id = :cid
	UNION ALL
		SELECT c.id, s.depth + 1
		FROM comments c JOIN subtree s ON c.parent_comment_id = s.id
		WHERE s.depth < :subtree_depth
), ancestors(id, parent_comment_id, depth) AS (
		SELECT id, parent_comment_id, 0 FROM comments WHERE id = :cid
	UNION ALL
		SELECT c.id, c.parent_comment_id, a.depth + 1
		FROM comments c JOIN ancestors a ON c.id = a.parent_comment_id
		WHERE a.depth < :ancestor_depth
)
SELECT id FROM subtree
UNION
SELECT id FROM ancestors
""")

@app.get("/comment/<cid>")
@app.get("/post/<pid>/<anything>/<cid>")
@auth_desired
def post_pid_comment_cid(cid, pid=None, anything=None, v=None):
	try: cid = int(cid)
	except: abort(404)
	comment = g.db.get(Comment, cid)
	if not comment: abort(404)

	if v and request.values.get("read"):
		notif = g.db.query(Notification).filter_by(comment_id=cid, user_id=v.id, read=False).one_or_none()
		if notif:
			notif.read = True
			g.db.add(notif)
			g.db.commit()

	# DM check — uses only column values, no relationships needed
	if not comment.parent_submission and not (v and (comment.author_id == v.id or comment.sentto == v.id)) and not (v and v.admin_level >= 2): abort(403)

	if not pid:
		if comment.parent_submission: pid = comment.parent_submission
		else: pid = 1

	post = get_post(pid, v=v)

	# Post-private check — uses already-loaded post
	if post.private and not (v and (v.admin_level >= 2 or v.id == post.author_id)): abort(403)

	if post.over_18 and not (v and v.over_18) and not session.get('over_18', 0) >= int(time.time()):
		if request.headers.get("Authorization"): return {'error': 'This content is not suitable for some users and situations.'}
		else: return render_template("errors/nsfw.html", v=v)

	if v: defaultsortingcomments = v.defaultsortingcomments
	else: defaultsortingcomments = "new"
	sort=request.values.get("sort", defaultsortingcomments)

	try: context = min(int(request.values.get("context", 0)), 8)
	except: context = 0

	# Load only the comments we render, not the whole top-comment thread.
	context_ids = [row[0] for row in g.db.execute(_CONTEXT_COMMENT_IDS_SQL, {
		"cid": comment.id,
		"subtree_depth": RENDER_DEPTH_LIMIT,
		"ancestor_depth": context + 1,
	})]

	def comment_tree_filter(q):
		return q.filter(Comment.id.in_(context_ids))

	comments, comment_tree = get_comment_trees_eager(comment_tree_filter, sort=sort, v=v, post=post)
	set_committed_value(post, 'comments', comments)

	comment_info = comment
	c = comment
	while context and c.level > 1:
		c.parent_comment.replies2 = [c]
		c = c.parent_comment
		context -= 1
	top_comment = c

	post.replies=[top_comment]
			
	if request.headers.get("Authorization"): return top_comment.json
	else: 
		if post.state_mod != StateMod.VISIBLE and not (v and (v.admin_level >= 2 or post.author_id == v.id)): template = "submission_banned.html"
		else: template = "submission.html"
		return render_template(template, v=v, p=post, sort=sort, comment_info=comment_info, render_replies=True)

@app.post("/comment")
@limiter.limit("1/second;20/minute;200/hour;1000/day")
@auth_required
def api_comment(v):
	if v.is_suspended: abort(403, "You can't perform this action while banned.")

	parent_fullname = request.values.get("parent_fullname", "").strip()

	if len(parent_fullname) < 4: abort(400)
	parent = None
	parent_post = None
	parent_comment_id = None

	if parent_fullname.startswith("post_"):
		parent = get_post(parent_fullname.split("post_")[1], v=v)
		parent_post = parent
	elif parent_fullname.startswith("comment_"):
		parent = get_comment(parent_fullname.split("comment_")[1], v=v)
		parent_post = get_post(parent.parent_submission, v=v) if parent.parent_submission else None
		parent_comment_id = parent.id
	else: abort(400)
	if not parent_post: abort(404) # don't allow sending comments to the ether
	level = 1 if isinstance(parent, Submission) else parent.level + 1

	body = sanitize_raw(request.values.get("body"), allow_newlines=True, length_limit=COMMENT_BODY_LENGTH_MAXIMUM)
	if not body and not request.files.get('file'): 
		abort(400, "You need to actually write something!")

	if request.files.get("file") and request.headers.get("cf-ipcountry") != "T1":
		files = request.files.getlist('file')[:4]
		for file in files:
			if file.content_type.startswith('image/'):
				oldname = f'/images/{time.time()}'.replace('.','') + '.webp'
				file.save(oldname)
				image = process_image(oldname)
				if image == "":
					abort(500, "Image upload failed")

				if app.config['MULTIMEDIA_EMBEDDING_ENABLED']:
					body += f"\n\n![]({image})"
				else:
					body += f'\n\n<a href="{image}">{image}</a>'
			else: abort(400, "Image files only")

	body_html = sanitize(body, comment=True)

	existing = g.db.query(Comment.id).filter(
		Comment.author_id == v.id,
		Comment.state_user_deleted_utc == None,
		Comment.parent_comment_id == parent_comment_id,
		Comment.parent_submission == parent_post.id,
		Comment.body_html == body_html
	).one_or_none()

	if existing:
		abort(409, f"You already made that comment: /comment/{existing.id}")

	replying_to_blocked = parent.author.is_blocking(v) and v.admin_level < 2

	is_bot = bool(request.headers.get("Authorization"))

	if not is_bot and len(body) > 10:
		now = int(time.time())
		cutoff = now - 60 * 60 * 24

		similar_comments = g.db.query(Comment).filter(
			Comment.author_id == v.id,
			Comment.body.op(
				'<->')(body) < app.config["COMMENT_SPAM_SIMILAR_THRESHOLD"],
			Comment.created_utc > cutoff
		).all()

		threshold = app.config["COMMENT_SPAM_COUNT_THRESHOLD"]
		if v.age_seconds >= (60 * 60 * 24 * 7):
			threshold *= 3
		elif v.age_seconds >= (60 * 60 * 24):
			threshold *= 2

		if len(similar_comments) > threshold:
			text = "Your account has been banned for **1 day** for the following reason:\n\n> Too much spam!"
			send_repeatable_notification(v.id, text)

			v.ban(reason="Spamming.",
					days=1)

			for comment in similar_comments:
				comment.state_mod = StateMod.REMOVED
				comment.state_mod_set_by = "AutoJanny"
				g.db.add(comment)
				ma=ModAction(
					user_id=AUTOJANNY_ID,
					target_comment_id=comment.id,
					kind="remove_comment",
					_note="spam"
					)
				g.db.add(ma)

			abort(403, "Too much spam!")

	is_filtered = v.should_comments_be_filtered

	if (v.admin_level <= PERMS['POST_COMMENT_MODERATION'] 
		and len(body) > COMMENT_BODY_LENGTH_MAXIMUM_UNFILTERED):
		is_filtered = True

	c = Comment(author_id=v.id,
				parent_submission=parent_post.id,
				parent_comment_id=parent_comment_id,
				level=level,
				over_18=parent_post.over_18 or request.values.get("over_18")=="true",
				is_bot=is_bot,
				app_id=v.client.application.id if v.client else None,
				body_html=body_html,
				body=body[:COMMENT_BODY_LENGTH_MAXIMUM],
				ghost=parent_post.ghost,
				state_mod=StateMod.FILTERED if is_filtered else StateMod.VISIBLE,
				)

	c.upvotes = 1
	g.db.add(c)
	g.db.flush()

	if c.level == 1: c.top_comment_id = c.id
	else: c.top_comment_id = parent.top_comment_id

	if not v.shadowbanned and not is_filtered:
		comment_on_publish(c)

	vote = CommentVote(user_id=v.id,
						 comment_id=c.id,
						 vote_type=1,
						 )
	g.db.add(vote)

	c.voted = 1

	g.db.commit()

	if request.headers.get("Authorization"): return c.json

	# Re-fetch with eager-loaded relationships so the comment template
	# (author popover/badges, awards, reports) doesn't lazy-load per-field.
	c = g.db.query(Comment).filter_by(id=c.id).options(
		selectinload(Comment.author).options(*author_load_options(v)),
		selectinload(Comment.post),
		selectinload(Comment.reports).options(
			selectinload(CommentFlag.user),
		),
		selectinload(Comment.awards).options(
			selectinload(AwardRelationship.user),
		),
		selectinload(Comment.senttouser),
	).one()
	c.voted = 1

	if replying_to_blocked:
		message = "This user has blocked you. You are still welcome to reply " \
				  "but you will be held to a higher standard of civility than you would be otherwise"
	elif (v.admin_level <= PERMS['POST_COMMENT_MODERATION']
			and len(body) > COMMENT_BODY_LENGTH_MAXIMUM_UNFILTERED):
		message = "Your comment has been submitted but is a bit long, so it's pending approval."
	else:
		message = None
	return {"comment": render_template("comments.html", v=v, comments=[c], ajax=True, parent_level=level), "message": message}

@app.post("/edit_comment/<cid>")
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@auth_required
def edit_comment(cid, v):
	c = get_comment(cid, v=v)
	if c.author_id != v.id: abort(403)
	body = sanitize_raw(request.values.get("body"), allow_newlines=True, length_limit=COMMENT_BODY_LENGTH_MAXIMUM)

	if len(body) < 1 and not (request.files.get("file") and request.headers.get("cf-ipcountry") != "T1"):
		abort(400, "You have to actually type something!")

	if body != c.body or request.files.get("file") and request.headers.get("cf-ipcountry") != "T1":
		body_html = sanitize(body, edit=True)

		# Spam Checking
		now = int(time.time())
		cutoff = now - 60 * 60 * 24

		similar_comments = g.db.query(Comment
		).filter(
			Comment.author_id == v.id,
			Comment.body.op(
				'<->')(body) < app.config["SPAM_SIMILARITY_THRESHOLD"],
			Comment.created_utc > cutoff
		).all()

		threshold = app.config["SPAM_SIMILAR_COUNT_THRESHOLD"]
		if v.age_seconds >= (60 * 60 * 24 * 30):
			threshold *= 4
		elif v.age_seconds >= (60 * 60 * 24 * 7):
			threshold *= 3
		elif v.age_seconds >= (60 * 60 * 24):
			threshold *= 2

		if len(similar_comments) > threshold:
			text = "Your account has been banned for **1 day** for the following reason:\n\n> Too much spam!"
			send_repeatable_notification(v.id, text)

			v.ban(reason="Spamming.",
					days=1)

			for comment in similar_comments:
				comment.state_mod = StateMod.REMOVED
				comment.state_mod_set_by = "AutoJanny"
				g.db.add(comment)

			abort(403, "Too much spam!")
		# End Spam Checking

		if request.files.get("file") and request.headers.get("cf-ipcountry") != "T1":
			files = request.files.getlist('file')[:4]
			for file in files:
				if file.content_type.startswith('image/'):
					name = f'/images/{time.time()}'.replace('.','') + '.webp'
					file.save(name)
					url = process_image(name)
					body += f"\n\n![]({url})"
				else: abort(400, "Image files only")

			body_html = sanitize(body, edit=True)

		c.body = body[:COMMENT_BODY_LENGTH_MAXIMUM]
		c.body_html = body_html

		if int(time.time()) - c.created_utc > 60 * 3: c.edited_utc = int(time.time())

		g.db.add(c)
		
		if c.state_mod == StateMod.VISIBLE:
			notify_users = NOTIFY_USERS(body, v)

			for x in notify_users:
				notif = g.db.query(Notification) \
					.filter_by(comment_id=c.id, user_id=x).one_or_none()
				if not notif:
					n = Notification(comment_id=c.id, user_id=x)
					g.db.add(n)

		g.db.commit()

	return {"comment": c.realbody(v)}


@app.post("/delete/comment/<cid>")
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@auth_required
def delete_comment(cid, v):
	c = get_comment(cid, v=v)
	if c.state_user_deleted_utc: abort(409)
	if c.author_id != v.id: abort(403)
	c.state_user_deleted_utc = datetime.now(tz=timezone.utc)
	# TODO: update stateful counters
	g.db.add(c)
	g.db.commit()

	return {"message": "Comment deleted!"}

@app.post("/undelete/comment/<cid>")
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@auth_required
def undelete_comment(cid, v):
	c = get_comment(cid, v=v)
	if not c.state_user_deleted_utc: abort(409)
	if c.author_id != v.id: abort(403)
	c.state_user_deleted_utc = None
	# TODO: update stateful counters
	g.db.add(c)
	g.db.commit()

	return {"message": "Comment undeleted!"}


@app.post("/pin_comment/<cid>")
@auth_required
def pin_comment(cid, v):
	comment = get_comment(cid, v=v)
	
	if not comment.is_pinned:
		if v.id != comment.post.author_id: abort(403)
		
		if comment.post.ghost: comment.is_pinned = "(OP)"
		else: comment.is_pinned = v.username + " (OP)"

		g.db.add(comment)

		if v.id != comment.author_id:
			if comment.post.ghost: message = f"OP has pinned your [comment]({comment.shortlink})!"
			else: message = f"@{v.username} (OP) has pinned your [comment]({comment.shortlink})!"
			send_repeatable_notification(comment.author_id, message)

		g.db.commit()
	return {"message": "Comment pinned!"}
	

@app.post("/unpin_comment/<cid>")
@auth_required
def unpin_comment(cid, v):
	
	comment = get_comment(cid, v=v)
	
	if comment.is_pinned:
		if v.id != comment.post.author_id: abort(403)

		if not comment.is_pinned.endswith(" (OP)"): 
			abort(403, "You can only unpin comments you have pinned!")

		comment.is_pinned = None
		g.db.add(comment)

		if v.id != comment.author_id:
			message = f"@{v.username} (OP) has unpinned your [comment]({comment.shortlink})!"
			send_repeatable_notification(comment.author_id, message)
		g.db.commit()
	return {"message": "Comment unpinned!"}


@app.post("/save_comment/<cid>")
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@auth_required
def save_comment(cid, v):
	comment=get_comment(cid)

	save=g.db.query(CommentSaveRelationship).filter_by(user_id=v.id, comment_id=comment.id).one_or_none()

	if not save:
		new_save=CommentSaveRelationship(user_id=v.id, comment_id=comment.id)
		g.db.add(new_save)

		g.db.commit()

	return {"message": "Comment saved!"}

@app.post("/unsave_comment/<cid>")
@limiter.limit("1/second;30/minute;200/hour;1000/day")
@auth_required
def unsave_comment(cid, v):
	comment=get_comment(cid)

	save=g.db.query(CommentSaveRelationship).filter_by(user_id=v.id, comment_id=comment.id).one_or_none()

	if save:
		g.db.delete(save)
		g.db.commit()

	return {"message": "Comment unsaved!"}
