import json
import secrets
from datetime import datetime, timedelta

from flask import request, jsonify, render_template, abort, session
from werkzeug.security import generate_password_hash, check_password_hash

from helpers import _db, get_current_user, get_plan_by_id, get_site_settings, static_url, get_effective_plan_for_user, log_deletion


def _derive_file_urls_for_job(job_id, user_id):
    """
    Return the list of absolute download URLs for a job that is owned by user_id.
    Raises ValueError if the job is not found or does not belong to the user.
    """
    try:
        from local_jobs import LocalJob
        job = LocalJob.fetch(job_id)
    except Exception:
        job = None

    with _db() as c:
        conv = c.execute(
            "SELECT user_id FROM conversions WHERE job_id=? ORDER BY id DESC LIMIT 1",
            (job_id,)
        ).fetchone()

    if not conv:
        raise ValueError("Job not found")

    if conv['user_id'] is None or conv['user_id'] != user_id:
        raise ValueError("You do not have access to this job")

    if job is None or not job.is_finished or not job.result:
        raise ValueError("Job output not available")

    if job.result.get('error'):
        raise ValueError("Job did not complete successfully")

    results = job.result.get('results') or []
    if not results:
        raise ValueError("No output files found for this job")

    urls = []
    for r in results:
        try:
            urls.append(static_url(r))
        except Exception:
            pass
    return urls


def register_sharing_routes(app):

    def _get_db():
        return _db()

    @app.route('/share/create', methods=['POST'])
    def share_create():
        user = get_current_user()
        if not user:
            return jsonify({'ok': False, 'error': 'Login required'}), 401

        plan = get_effective_plan_for_user(user['id'])
        if not plan or not int(plan.get('share_link_enabled', 0)):
            return jsonify({'ok': False, 'error': 'Share links not available on your plan'}), 403

        data = request.get_json(silent=True) or {}
        job_id = data.get('job_id', '').strip()
        password = data.get('password', '').strip()
        is_one_time = bool(data.get('is_one_time', False))
        expires_in_hours = data.get('expires_in_hours')

        if not job_id:
            return jsonify({'ok': False, 'error': 'job_id required'}), 400

        if password and not int(plan.get('share_link_password', 0)):
            return jsonify({'ok': False, 'error': 'Password protection not available on your plan'}), 403

        if is_one_time and not int(plan.get('share_link_onetime', 0)):
            return jsonify({'ok': False, 'error': 'One-time links not available on your plan'}), 403

        if expires_in_hours and not int(plan.get('share_link_expiry', 0)):
            return jsonify({'ok': False, 'error': 'Custom expiry not available on your plan'}), 403

        max_per_job = int(plan.get('share_link_max_per_job', 0))
        if max_per_job > 0:
            with _get_db() as _cntdb:
                _existing = _cntdb.execute(
                    "SELECT COUNT(*) FROM shared_links WHERE job_id=? AND user_id=?",
                    (job_id, user['id'])
                ).fetchone()[0]
            if _existing >= max_per_job:
                return jsonify({'ok': False, 'error': 'Share link limit reached for this file ({}/{})'.format(_existing, max_per_job)}), 403

        try:
            file_urls = _derive_file_urls_for_job(job_id, user['id'])
        except ValueError as e:
            return jsonify({'ok': False, 'error': str(e)}), 403
        except Exception:
            return jsonify({'ok': False, 'error': 'Could not read job output files'}), 500

        max_expiry = int(plan.get('share_link_max_expiry_hours', 0))
        expires_at = None
        if expires_in_hours:
            try:
                expires_in_hours = int(expires_in_hours)
                if expires_in_hours > 0:
                    if max_expiry > 0 and expires_in_hours > max_expiry:
                        expires_in_hours = max_expiry
                    expires_at = (datetime.utcnow() + timedelta(hours=expires_in_hours)).strftime('%Y-%m-%d %H:%M:%S')
            except (ValueError, TypeError):
                pass

        password_hash = generate_password_hash(password) if password else None
        token = secrets.token_urlsafe(24)
        file_urls_json = json.dumps(file_urls)

        with _get_db() as c:
            c.execute(
                "INSERT INTO shared_links (token, job_id, user_id, password_hash, is_one_time, expires_at, file_urls) "
                "VALUES (?,?,?,?,?,?,?)",
                (token, job_id, user['id'], password_hash, 1 if is_one_time else 0, expires_at, file_urls_json)
            )
            c.commit()

        site = get_site_settings()
        site_url = (site.get('site_url', '') or '').strip().rstrip('/')
        if site_url:
            base_url = site_url
        else:
            from flask import current_app
            server_name = (current_app.config.get('SERVER_NAME') or '').lower()
            host = request.host.split(':')[0].lower()
            if server_name and host != server_name and host.endswith('.' + server_name):
                base_url = '{}://{}'.format(request.scheme, server_name)
            else:
                base_url = request.host_url.rstrip('/')
        share_url = "{}/s/{}".format(base_url, token)

        return jsonify({'ok': True, 'url': share_url, 'token': token})

    @app.route('/share/grant', methods=['POST'])
    def share_direct_grant():
        """Grant file access directly by job_id + email — no share link required."""
        user = get_current_user()
        if not user:
            return jsonify({'ok': False, 'error': 'Login required'}), 401

        data = request.get_json(silent=True) or {}
        job_id = (data.get('job_id') or '').strip()
        invite_email = (data.get('email') or '').strip().lower()

        if not job_id:
            return jsonify({'ok': False, 'error': 'job_id required'}), 400
        if not invite_email or '@' not in invite_email:
            return jsonify({'ok': False, 'error': 'Valid email address required'}), 400

        # Verify requester owns the job
        with _get_db() as c:
            conv = c.execute(
                "SELECT user_id FROM conversions WHERE job_id=? LIMIT 1", (job_id,)
            ).fetchone()
        if not conv:
            return jsonify({'ok': False, 'error': 'File not found'}), 404
        if conv['user_id'] != user['id']:
            return jsonify({'ok': False, 'error': 'You do not own this file'}), 403
        if invite_email == (user.get('email') or '').strip().lower():
            return jsonify({'ok': False, 'error': 'You cannot grant access to yourself'}), 400

        # Look up invited user by email (case-insensitive)
        with _get_db() as c:
            u_row = c.execute(
                "SELECT id FROM users WHERE LOWER(email)=? LIMIT 1", (invite_email,)
            ).fetchone()
        granted_uid = u_row['id'] if u_row else None

        # Write the grant — use a two-phase approach so old databases that lack
        # the granted_by_user_id column still work correctly.
        grant_row = None
        try:
            with _get_db() as c:
                try:
                    c.execute(
                        "INSERT OR IGNORE INTO job_access_grants "
                        "(job_id, granted_to_email, granted_to_user_id, granted_by_user_id) "
                        "VALUES (?,?,?,?)",
                        (job_id, invite_email, granted_uid, user['id'])
                    )
                except Exception as _insert_err:
                    # Only fall back for SQLite missing-column schema errors:
                    #   "table X has no column named Y"  (INSERT with unknown column)
                    #   "no such column: Y"              (UPDATE/WHERE reference)
                    # Re-raise anything else (disk errors, constraint violations, etc.)
                    _err_str = str(_insert_err).lower()
                    if 'has no column named' not in _err_str and 'no such column' not in _err_str:
                        raise
                    # Column granted_by_user_id does not exist on this (older) database —
                    # fall back to the three-column form which always exists.
                    c.execute(
                        "INSERT OR IGNORE INTO job_access_grants "
                        "(job_id, granted_to_email, granted_to_user_id) "
                        "VALUES (?,?,?)",
                        (job_id, invite_email, granted_uid)
                    )
                if granted_uid:
                    try:
                        c.execute(
                            "UPDATE job_access_grants SET granted_to_user_id=?, granted_by_user_id=? "
                            "WHERE job_id=? AND LOWER(granted_to_email)=? AND granted_to_user_id IS NULL",
                            (granted_uid, user['id'], job_id, invite_email)
                        )
                    except Exception as _upd_err:
                        _err_str = str(_upd_err).lower()
                        if 'has no column named' not in _err_str and 'no such column' not in _err_str:
                            raise
                        c.execute(
                            "UPDATE job_access_grants SET granted_to_user_id=? "
                            "WHERE job_id=? AND LOWER(granted_to_email)=? AND granted_to_user_id IS NULL",
                            (granted_uid, job_id, invite_email)
                        )
                c.commit()
                grant_row = c.execute(
                    "SELECT id, granted_to_email, granted_at FROM job_access_grants "
                    "WHERE job_id=? AND LOWER(granted_to_email)=? LIMIT 1",
                    (job_id, invite_email)
                ).fetchone()
        except Exception as _ge:
            from flask import current_app
            current_app.logger.warning(f"share_direct_grant: DB error: {_ge}")
            return jsonify({'ok': False, 'error': 'Failed to record access grant'}), 500

        # Build invite email
        site = get_site_settings()
        site_url = (site.get('site_url', '') or '').strip().rstrip('/')
        if site_url:
            base_url = site_url
        else:
            from flask import current_app
            server_name = (current_app.config.get('SERVER_NAME') or '').lower()
            host = request.host.split(':')[0].lower()
            if server_name and host != server_name and host.endswith('.' + server_name):
                base_url = '{}://{}'.format(request.scheme, server_name)
            else:
                base_url = request.host_url.rstrip('/')

        output_url = '{}/output/{}'.format(base_url, job_id)
        site_name = site.get('site_name', 'OnlineConvert').strip() or 'OnlineConvert'
        sharer_name = (user.get('username') or user.get('email') or 'Someone').strip()
        sharer_email = (user.get('email') or '').strip()

        subject = '{} shared files with you on {}'.format(sharer_name, site_name)

        html_body = """
<div style="font-family:Arial,sans-serif;max-width:560px;margin:0 auto;padding:24px;background:#f9fafb;border-radius:12px">
  <h2 style="color:#1e3a5f;margin-bottom:8px">&#128193; You've been invited to access a shared file</h2>
  <p style="color:#374151;font-size:15px;margin-bottom:4px">
    <strong>{sharer}</strong>{via} has shared converted files with you via <strong>{site}</strong>.
  </p>
  <p style="color:#6b7280;font-size:13px;margin-bottom:24px">Click the button below to view and download the files. You'll need to log in (or create a free account) to access them.</p>
  <a href="{url}" style="display:inline-block;padding:12px 28px;background:#3B82F6;color:#fff;border-radius:8px;text-decoration:none;font-weight:600;font-size:15px">
    &#128229; View Shared Files
  </a>
  <p style="color:#9ca3af;font-size:12px;margin-top:24px;border-top:1px solid #e5e7eb;padding-top:16px">
    If the button doesn't work, copy this link: <a href="{url}" style="color:#3B82F6">{url}</a><br>
    This invitation was sent by {sharer} ({sharer_email}) using {site}.
  </p>
</div>""".format(
            sharer=sharer_name,
            via=' ({})'.format(sharer_email) if sharer_email else '',
            site=site_name,
            url=output_url,
            sharer_email=sharer_email or sharer_name,
        )

        text_body = (
            '{sharer} has shared files with you on {site}.\n\n'
            'Log in and view the files here:\n{url}\n\n'
            'Shared by: {sharer} ({sharer_email})'
        ).format(
            sharer=sharer_name,
            site=site_name,
            url=output_url,
            sharer_email=sharer_email or sharer_name,
        )

        from helpers import send_email
        email_ok, email_err = send_email(invite_email, subject, html_body, text_body)
        grant = dict(grant_row) if grant_row else {'granted_to_email': invite_email}
        if not email_ok:
            # The access grant was saved successfully — return success but include a
            # warning so the UI can inform the user that the notification email was
            # not delivered (e.g. SMTP not configured).
            return jsonify({
                'ok': True,
                'grant': grant,
                'email_warning': email_err or 'Access granted, but the notification email could not be sent. Check SMTP settings in admin.',
            })

        return jsonify({'ok': True, 'grant': grant})

    @app.route('/s/<token>', methods=['GET', 'POST'])
    def share_view(token):
        with _get_db() as c:
            row = c.execute(
                "SELECT sl.*, u.username FROM shared_links sl "
                "LEFT JOIN users u ON u.id = sl.user_id "
                "WHERE sl.token=? LIMIT 1",
                (token,)
            ).fetchone()

        if not row:
            abort(404)

        row = dict(row)

        now = datetime.utcnow()
        if row.get('expires_at'):
            try:
                exp = datetime.strptime(row['expires_at'], '%Y-%m-%d %H:%M:%S')
                if now > exp:
                    return render_template('share.html', expired=True, token=token,
                                           sharer=row.get('username', 'Someone'))
            except Exception:
                pass

        if row.get('is_one_time') and row.get('is_accessed'):
            return render_template('share.html', expired=True, one_time_used=True, token=token,
                                   sharer=row.get('username', 'Someone'))

        needs_password = bool(row.get('password_hash'))
        password_error = None

        if needs_password:
            if request.method == 'POST':
                entered = request.form.get('password', '')
                if check_password_hash(row['password_hash'], entered):
                    if row.get('is_one_time') and not row.get('is_accessed'):
                        with _get_db() as c:
                            c.execute("UPDATE shared_links SET is_accessed=1 WHERE token=?", (token,))
                            c.commit()
                    try:
                        file_urls = json.loads(row.get('file_urls', '[]'))
                    except Exception:
                        file_urls = []
                    return render_template('share.html', file_urls=file_urls, row=row,
                                           sharer=row.get('username', 'Someone'),
                                           expires_at=row.get('expires_at'))
                else:
                    password_error = 'Incorrect password. Please try again.'
            return render_template('share.html', needs_password=True, token=token,
                                   password_error=password_error,
                                   sharer=row.get('username', 'Someone'))

        if row.get('is_one_time') and not row.get('is_accessed'):
            with _get_db() as c:
                c.execute("UPDATE shared_links SET is_accessed=1 WHERE token=?", (token,))
                c.commit()

        try:
            file_urls = json.loads(row.get('file_urls', '[]'))
        except Exception:
            file_urls = []

        return render_template('share.html', file_urls=file_urls, row=row,
                               sharer=row.get('username', 'Someone'),
                               expires_at=row.get('expires_at'))

    @app.route('/share/<token>', methods=['DELETE'])
    @app.route('/share/<token>/revoke', methods=['POST'])
    def share_delete(token):
        user = get_current_user()
        is_admin_session = bool(session.get('login'))
        if not user and not is_admin_session:
            return jsonify({'ok': False, 'error': 'Login required'}), 401
        with _get_db() as c:
            row = c.execute("SELECT user_id, job_id FROM shared_links WHERE token=?", (token,)).fetchone()
            if not row:
                return jsonify({'ok': False, 'error': 'Not found'}), 404
            if not is_admin_session and row['user_id'] != (user['id'] if user else None):
                return jsonify({'ok': False, 'error': 'Forbidden'}), 403
            c.execute("DELETE FROM shared_links WHERE token=?", (token,))
            c.commit()
        _actor_id = user['id'] if user else None
        _actor_name = user.get('username', '') if user else session.get('admin_username', 'admin')
        _actor_role = 'admin' if is_admin_session else 'user'
        log_deletion('share', entity_id=token, entity_name=token,
                     actor_user_id=_actor_id, actor_username=_actor_name,
                     actor_role=_actor_role,
                     extra_meta={'job_id': row['job_id'] if row else None, 'token': token},
                     entity_owner_user_id=row['user_id'] if row else None)
        return jsonify({'ok': True})

    @app.route('/share/list/<job_id>', methods=['GET'])
    def share_list(job_id):
        user = get_current_user()
        is_admin_session = bool(session.get('login'))
        if not user and not is_admin_session:
            return jsonify({'ok': False, 'error': 'Login required'}), 401
        plan = get_effective_plan_for_user(user['id']) if user else get_plan_by_id(1)
        plan_limit = int((plan or {}).get('share_link_max_per_job', 0))
        with _get_db() as c:
            if is_admin_session:
                rows = c.execute(
                    "SELECT token, is_one_time, is_accessed, expires_at, created_at, "
                    "CASE WHEN password_hash IS NOT NULL AND password_hash != '' THEN 1 ELSE 0 END AS has_password "
                    "FROM shared_links WHERE job_id=? ORDER BY id DESC",
                    (job_id,)
                ).fetchall()
            else:
                rows = c.execute(
                    "SELECT token, is_one_time, is_accessed, expires_at, created_at, "
                    "CASE WHEN password_hash IS NOT NULL AND password_hash != '' THEN 1 ELSE 0 END AS has_password "
                    "FROM shared_links WHERE job_id=? AND user_id=? ORDER BY id DESC",
                    (job_id, user['id'])
                ).fetchall()
        site = get_site_settings()
        site_url = (site.get('site_url', '') or '').strip().rstrip('/')
        if site_url:
            base_url = site_url
        else:
            from flask import current_app
            server_name = (current_app.config.get('SERVER_NAME') or '').lower()
            host = request.host.split(':')[0].lower()
            if server_name and host != server_name and host.endswith('.' + server_name):
                base_url = '{}://{}'.format(request.scheme, server_name)
            else:
                base_url = request.host_url.rstrip('/')
        links = []
        for r in rows:
            r = dict(r)
            r['url'] = "{}/s/{}".format(base_url, r['token'])
            links.append(r)
        return jsonify({'ok': True, 'links': links, 'count': len(links), 'plan_limit': plan_limit})

    @app.route('/share/grants/<job_id>', methods=['GET'])
    def share_grants_list(job_id):
        """Return email-based access grants the owner has issued for a job."""
        user = get_current_user()
        is_admin_session = bool(session.get('login'))
        if not user and not is_admin_session:
            return jsonify({'ok': False, 'error': 'Login required'}), 401
        with _get_db() as c:
            conv = c.execute(
                "SELECT user_id FROM conversions WHERE job_id=? LIMIT 1", (job_id,)
            ).fetchone()
            if not is_admin_session and (not conv or (conv['user_id'] and conv['user_id'] != (user['id'] if user else None))):
                return jsonify({'ok': False, 'error': 'Forbidden'}), 403
            rows = c.execute(
                "SELECT id, granted_to_email, granted_to_user_id, granted_at "
                "FROM job_access_grants WHERE job_id=? ORDER BY id ASC",
                (job_id,)
            ).fetchall()
        return jsonify({'ok': True, 'grants': [dict(r) for r in rows]})

    @app.route('/share/grants/revoke', methods=['POST'])
    def share_grant_revoke():
        """Revoke a specific email-based access grant (owner or admin)."""
        user = get_current_user()
        is_admin_session = bool(session.get('login'))
        if not user and not is_admin_session:
            return jsonify({'ok': False, 'error': 'Login required'}), 401
        data = request.get_json(silent=True) or {}
        grant_id = data.get('grant_id')
        job_id = data.get('job_id')
        if not grant_id or not job_id:
            return jsonify({'ok': False, 'error': 'Missing grant_id or job_id'}), 400
        with _get_db() as c:
            conv = c.execute(
                "SELECT user_id FROM conversions WHERE job_id=? LIMIT 1", (job_id,)
            ).fetchone()
            if not is_admin_session and (not conv or (conv['user_id'] and conv['user_id'] != (user['id'] if user else None))):
                return jsonify({'ok': False, 'error': 'Forbidden'}), 403
            _grant_row = c.execute(
                "SELECT granted_to_email, granted_to_user_id FROM job_access_grants WHERE id=? AND job_id=?",
                (grant_id, job_id)
            ).fetchone()
            c.execute(
                "DELETE FROM job_access_grants WHERE id=? AND job_id=?",
                (grant_id, job_id)
            )
            c.commit()
        _actor_id = user['id'] if user else None
        _actor_name = user.get('username', '') if user else session.get('admin_username', 'admin')
        _actor_role = 'admin' if is_admin_session else 'user'
        _grantee = (_grant_row['granted_to_email'] if _grant_row else '') or str(grant_id)
        _owner_id = conv['user_id'] if conv else None
        log_deletion('share_grant', entity_id=grant_id, entity_name=_grantee,
                     actor_user_id=_actor_id, actor_username=_actor_name,
                     actor_role=_actor_role,
                     extra_meta={'job_id': job_id, 'grant_id': grant_id,
                                 'granted_to_user_id': _grant_row['granted_to_user_id'] if _grant_row else None},
                     entity_owner_user_id=_owner_id)
        return jsonify({'ok': True})

    @app.route('/share/invite', methods=['POST'])
    def share_invite():
        user = get_current_user()
        if not user:
            return jsonify({'ok': False, 'error': 'Login required'}), 401

        plan = get_effective_plan_for_user(user['id'])
        if not plan or not int(plan.get('share_link_enabled', 0)):
            return jsonify({'ok': False, 'error': 'Share links not available on your plan'}), 403

        data = request.get_json(silent=True) or {}
        token = (data.get('token') or '').strip()
        invite_email = (data.get('email') or '').strip().lower()

        if not token:
            return jsonify({'ok': False, 'error': 'token required'}), 400
        if not invite_email or '@' not in invite_email:
            return jsonify({'ok': False, 'error': 'Valid email address required'}), 400

        with _get_db() as c:
            row = c.execute(
                "SELECT * FROM shared_links WHERE token=? AND user_id=? LIMIT 1",
                (token, user['id'])
            ).fetchone()

        if not row:
            return jsonify({'ok': False, 'error': 'Share link not found or not owned by you'}), 404

        site = get_site_settings()
        site_url = (site.get('site_url', '') or '').strip().rstrip('/')
        if site_url:
            base_url = site_url
        else:
            from flask import current_app
            server_name = (current_app.config.get('SERVER_NAME') or '').lower()
            host = request.host.split(':')[0].lower()
            if server_name and host != server_name and host.endswith('.' + server_name):
                base_url = '{}://{}'.format(request.scheme, server_name)
            else:
                base_url = request.host_url.rstrip('/')

        share_url = "{}/s/{}".format(base_url, token)
        _grant_job_id_for_email = dict(row).get('job_id', '')
        output_url = "{}/output/{}".format(base_url, _grant_job_id_for_email) if _grant_job_id_for_email else ''
        site_name = site.get('site_name', 'OnlineConvert').strip() or 'OnlineConvert'
        sharer_name = (user.get('username') or user.get('email') or 'Someone').strip()
        sharer_email = (user.get('email') or '').strip()

        subject = '{} shared files with you on {}'.format(sharer_name, site_name)

        # Include output page URL so registered/logged-in users can access the
        # full conversion result directly, in addition to the public share link.
        _output_section = ''
        if output_url:
            _output_section = (
                '<div style="margin-top:16px;padding:12px 16px;background:#eff6ff;border-radius:8px;border:1px solid #bfdbfe">'
                '<p style="color:#1e40af;font-size:13px;margin:0 0 6px 0;font-weight:600">Have an account on {site}?</p>'
                '<p style="color:#3b82f6;font-size:12px;margin:0 0 8px 0">'
                'Log in and visit the full result page for download options, previews, and more:</p>'
                '<a href="{ourl}" style="color:#3B82F6;font-size:12px;word-break:break-all">{ourl}</a>'
                '</div>'
            ).format(site=site_name, ourl=output_url)

        html_body = """
<div style="font-family:Arial,sans-serif;max-width:560px;margin:0 auto;padding:24px;background:#f9fafb;border-radius:12px">
  <h2 style="color:#1e3a5f;margin-bottom:8px">&#128193; You've been invited to access a shared file</h2>
  <p style="color:#374151;font-size:15px;margin-bottom:4px">
    <strong>{sharer}</strong>{via} has shared converted files with you via <strong>{site}</strong>.
  </p>
  <p style="color:#6b7280;font-size:13px;margin-bottom:24px">Click the button below to access the files. No account required.</p>
  <a href="{url}" style="display:inline-block;padding:12px 28px;background:#3B82F6;color:#fff;border-radius:8px;text-decoration:none;font-weight:600;font-size:15px">
    &#128229; View Shared Files
  </a>
  {output_section}
  <p style="color:#9ca3af;font-size:12px;margin-top:24px;border-top:1px solid #e5e7eb;padding-top:16px">
    If the button doesn't work, copy this link: <a href="{url}" style="color:#3B82F6">{url}</a><br>
    This invitation was sent by {sharer} ({sharer_email}) using {site}.
  </p>
</div>""".format(
            sharer=sharer_name,
            via=' ({})'.format(sharer_email) if sharer_email else '',
            site=site_name,
            url=share_url,
            sharer_email=sharer_email or sharer_name,
            output_section=_output_section,
        )

        text_body = (
            '{sharer} has shared files with you on {site}.\n\n'
            'Access the files here (no login needed):\n{url}\n\n'
            '{output_line}'
            'Shared by: {sharer} ({sharer_email})'
        ).format(
            sharer=sharer_name,
            site=site_name,
            url=share_url,
            sharer_email=sharer_email or sharer_name,
            output_line=(
                'If you have a {site} account, log in and visit the full result page:\n{ourl}\n\n'.format(
                    site=site_name, ourl=output_url
                ) if output_url else ''
            ),
        )

        from helpers import send_email
        ok, err = send_email(invite_email, subject, html_body, text_body)
        if not ok:
            return jsonify({'ok': False, 'error': err or 'Failed to send email. Check SMTP settings in admin.'}), 500

        # Record a DB-level access grant so the invited user can also access
        # /output/<job_id> directly once they are logged in.
        _grant_job_id = dict(row).get('job_id', '')
        if _grant_job_id:
            try:
                with _get_db() as _gc:
                    _u_row = _gc.execute(
                        "SELECT id FROM users WHERE email=? LIMIT 1",
                        (invite_email,)
                    ).fetchone()
                    _granted_uid = _u_row['id'] if _u_row else None
                    _gc.execute(
                        "INSERT OR IGNORE INTO job_access_grants "
                        "(job_id, granted_to_email, granted_to_user_id) VALUES (?,?,?)",
                        (_grant_job_id, invite_email, _granted_uid)
                    )
                    _gc.commit()
            except Exception as _ge:
                from flask import current_app
                current_app.logger.warning(
                    f"share_invite: failed to write access grant for job "
                    f"{_grant_job_id} / {invite_email}: {_ge}"
                )

        return jsonify({'ok': True})

    @app.route('/output/feedback/<job_id>', methods=['POST'])
    def output_feedback(job_id):
        ip = request.headers.get('X-Forwarded-For', request.remote_addr or '')
        if ip:
            ip = ip.split(',')[0].strip()

        # Session-level guard: track jobs this session has already rated
        _rated_key = '_feedback_rated'
        rated_jobs = session.get(_rated_key, [])
        if job_id in rated_jobs:
            return jsonify({'ok': True, 'duplicate': True})

        with _get_db() as c:
            # Duplicate guard: same IP + same job
            existing = c.execute(
                "SELECT id FROM job_feedback WHERE job_id=? AND ip_address=? LIMIT 1",
                (job_id, ip)
            ).fetchone()
            if existing:
                return jsonify({'ok': True, 'duplicate': True})
            # Time-window rate limit: max 10 feedback submissions per IP+session per hour
            recent_count = c.execute(
                "SELECT COUNT(*) FROM job_feedback WHERE ip_address=? AND created_at >= datetime('now', '-1 hour')",
                (ip,)
            ).fetchone()[0]
            if recent_count >= 10:
                return jsonify({'ok': False, 'error': 'rate_limited'}), 429

        data = request.get_json(silent=True) or {}
        rating = data.get('rating')
        comment = (data.get('comment', '') or '')[:2000]
        try:
            if rating is not None:
                rating = int(rating)
                if not (1 <= rating <= 5):
                    rating = None
        except (ValueError, TypeError):
            rating = None

        with _get_db() as c:
            c.execute(
                "INSERT INTO job_feedback (job_id, rating, comment, ip_address) VALUES (?,?,?,?)",
                (job_id, rating, comment, ip)
            )
            c.commit()

        # Record in session so the same browser session can't rate twice
        rated_jobs = session.get(_rated_key, [])
        rated_jobs.append(job_id)
        session[_rated_key] = rated_jobs[:50]  # cap to avoid session bloat
        return jsonify({'ok': True})
