import os
import secrets
from datetime import timedelta
from logging.handlers import RotatingFileHandler
from pathlib import Path
from zoneinfo import ZoneInfo

import click
from dotenv import load_dotenv
from flask import Flask, flash, redirect, render_template, request, url_for
from flask_login import current_user
from flask_wtf.csrf import CSRFError
from werkzeug.middleware.proxy_fix import ProxyFix

from .extensions import csrf, db, login_manager
from .policy import COMPANY_NAME, POLICY_REFERENCE, utcnow


def create_app(test_config=None):
    load_dotenv()
    app = Flask(__name__, instance_relative_config=True)
    root = Path(app.root_path).parent
    database_url = os.getenv("DATABASE_URL") or f"sqlite:///{root / 'instance' / 'ovr.db'}"
    if database_url.startswith("mysql://"):
        database_url = database_url.replace("mysql://", "mysql+pymysql://", 1)
    app.config.from_mapping(
        SECRET_KEY=os.getenv("SECRET_KEY", "development-only-change-me"),
        SQLALCHEMY_DATABASE_URI=database_url,
        SQLALCHEMY_TRACK_MODIFICATIONS=False,
        MAX_CONTENT_LENGTH=int(os.getenv("MAX_CONTENT_LENGTH_MB", "12")) * 1024 * 1024,
        UPLOAD_FOLDER=str(root / "storage" / "uploads"),
        SESSION_COOKIE_HTTPONLY=True,
        SESSION_COOKIE_SECURE=os.getenv("SESSION_COOKIE_SECURE", "false").lower() == "true",
        SESSION_COOKIE_SAMESITE="Lax",
        PERMANENT_SESSION_LIFETIME=timedelta(minutes=45),
        REMEMBER_COOKIE_HTTPONLY=True,
        REMEMBER_COOKIE_SECURE=os.getenv("SESSION_COOKIE_SECURE", "false").lower() == "true",
        MAIL_ENABLED=os.getenv("MAIL_ENABLED", "false").lower() == "true",
        MAIL_HOST=os.getenv("MAIL_HOST", ""),
        MAIL_PORT=int(os.getenv("MAIL_PORT", "587")),
        MAIL_USERNAME=os.getenv("MAIL_USERNAME", ""),
        MAIL_PASSWORD=os.getenv("MAIL_PASSWORD", ""),
        MAIL_USE_TLS=os.getenv("MAIL_USE_TLS", "true").lower() == "true",
        MAIL_FROM=os.getenv("MAIL_FROM", "ovr@example.com"),
        APP_BASE_URL=os.getenv("APP_BASE_URL", ""),
        APP_TIMEZONE=os.getenv("APP_TIMEZONE", "Asia/Riyadh"),
        SQLALCHEMY_ENGINE_OPTIONS={"pool_pre_ping": True, "pool_recycle": 280},
    )
    if test_config:
        app.config.update(test_config)
    Path(app.instance_path).mkdir(parents=True, exist_ok=True)
    Path(app.config["UPLOAD_FOLDER"]).mkdir(parents=True, exist_ok=True)
    log_dir = root / "storage" / "logs"
    log_dir.mkdir(parents=True, exist_ok=True)
    handler = RotatingFileHandler(log_dir / "app.log", maxBytes=2_000_000, backupCount=5)
    handler.setLevel("INFO")
    app.logger.addHandler(handler)

    app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)
    db.init_app(app)
    login_manager.init_app(app)
    csrf.init_app(app)

    from .auth import bp as auth_bp
    from .main import bp as main_bp
    from .admin import bp as admin_bp

    app.register_blueprint(auth_bp)
    app.register_blueprint(main_bp)
    app.register_blueprint(admin_bp)

    @app.template_filter("localdt")
    def local_datetime(value, fmt="%Y-%m-%d %H:%M"):
        if value is None:
            return ""
        if not hasattr(value, "tzinfo"):
            return value.strftime(fmt) if hasattr(value, "strftime") else str(value)
        from datetime import timezone

        source = value.replace(tzinfo=timezone.utc) if value.tzinfo is None else value
        return source.astimezone(ZoneInfo(app.config["APP_TIMEZONE"])).strftime(fmt)

    @app.before_request
    def enforce_password_change():
        allowed = {"auth.account", "auth.logout", "static"}
        if current_user.is_authenticated and current_user.must_change_password and request.endpoint not in allowed:
            flash("Change the temporary password before using the reporting system.", "warning")
            return redirect(url_for("auth.account"))

    @app.context_processor
    def inject_globals():
        from flask_login import current_user
        from .models import Notification

        unread = 0
        if current_user.is_authenticated:
            unread = db.session.scalar(
                db.select(db.func.count(Notification.id)).where(
                    Notification.user_id == current_user.id, Notification.read_at.is_(None)
                )
            ) or 0
        return {"company_name": COMPANY_NAME, "policy_reference": POLICY_REFERENCE, "unread_notifications": unread}

    @app.after_request
    def security_headers(response):
        response.headers.setdefault("X-Content-Type-Options", "nosniff")
        response.headers.setdefault("X-Frame-Options", "SAMEORIGIN")
        response.headers.setdefault("Referrer-Policy", "strict-origin-when-cross-origin")
        response.headers.setdefault("Permissions-Policy", "camera=(), microphone=(), geolocation=()")
        response.headers.setdefault(
            "Content-Security-Policy",
            "default-src 'self'; img-src 'self' data:; style-src 'self'; script-src 'self'; font-src 'self'; frame-ancestors 'self'",
        )
        if response.mimetype == "text/html":
            response.headers.setdefault("Cache-Control", "no-store")
        return response

    @app.errorhandler(CSRFError)
    def csrf_error(error):
        return render_template("error.html", title="Security check failed", message=error.description), 400

    @app.errorhandler(403)
    def forbidden(_error):
        return render_template("error.html", title="Access denied", message="You do not have permission to access this resource."), 403

    @app.errorhandler(404)
    def not_found(_error):
        return render_template("error.html", title="Not found", message="The requested page could not be found."), 404

    @app.errorhandler(413)
    def too_large(_error):
        return render_template("error.html", title="File too large", message="The uploaded file exceeds the configured size limit."), 413

    @app.errorhandler(500)
    def internal_error(error):
        reference = secrets.token_hex(6)
        db.session.rollback()
        app.logger.exception("Unhandled error reference %s: %s", reference, error)
        return render_template(
            "error.html",
            title="Unexpected application error",
            message=f"Please provide error reference {reference} to the system administrator.",
        ), 500

    @app.cli.command("init-db")
    @click.option("--admin-username", default=lambda: os.getenv("INITIAL_ADMIN_USERNAME", "admin"))
    @click.option("--admin-email", default=lambda: os.getenv("INITIAL_ADMIN_EMAIL", ""))
    @click.option("--admin-password", prompt=True, hide_input=True, confirmation_prompt=True)
    def init_db_command(admin_username, admin_email, admin_password):
        """Create tables, policy roles, and the first administrator."""
        from .services import seed_reference_data

        if len(admin_password) < 12:
            raise click.ClickException("Administrator password must contain at least 12 characters.")
        db.create_all()
        admin = seed_reference_data(admin_username, admin_email, admin_password)
        click.echo(f"Database initialized. Administrator: {admin.username if admin else admin_username}")

    @app.cli.command("send-due-reminders")
    def send_due_reminders_command():
        """Create in-app/email reminders for due and overdue OVR work."""
        from datetime import timedelta
        from .models import ActionItem, Incident, Notification
        from .services import notify

        now = utcnow()
        since = now - timedelta(hours=20)
        created = 0

        def once(user, title, message, incident):
            nonlocal created
            if not user:
                return
            exists = db.session.scalar(
                db.select(Notification.id).where(
                    Notification.user_id == user.id,
                    Notification.incident_id == incident.id,
                    Notification.title == title,
                    Notification.created_at >= since,
                )
            )
            if not exists:
                notify(user, title, message, incident)
                created += 1

        incidents = db.session.scalars(
            db.select(Incident).where(Incident.status != "CLOSED")
        ).all()
        for incident in incidents:
            if incident.investigation_due_at and incident.investigation_due_at <= now + timedelta(days=1):
                title = "OVR investigation overdue" if incident.investigation_due_at < now else "OVR investigation due soon"
                recipient = incident.assigned_manager if incident.status in {"SUBMITTED", "ACTION_REQUIRED"} else incident.assigned_qps
                once(recipient, title, f"{incident.reference_no} requires attention by {incident.investigation_due_at:%Y-%m-%d}.", incident)
            if incident.status == "QPS_FINAL" and incident.qps_closure_due_at and incident.qps_closure_due_at <= now + timedelta(days=1):
                once(incident.assigned_qps, "OVR closure due", f"Final closure is due for {incident.reference_no}.", incident)
        actions = db.session.scalars(
            db.select(ActionItem).where(
                ActionItem.status == "OPEN", ActionItem.due_at.is_not(None), ActionItem.due_at <= now + timedelta(days=1)
            )
        ).all()
        for item in actions:
            once(item.owner, "OVR corrective action due", f"Your action for {item.incident.reference_no} is due.", item.incident)
        db.session.commit()
        click.echo(f"Created {created} due reminder(s).")

    return app
