# Force UTF-8 stdout/stderr on Windows so emoji in print() calls don't crash
import sys, io
if sys.stdout.encoding and sys.stdout.encoding.lower() not in ('utf-8', 'utf8'):
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
if sys.stderr.encoding and sys.stderr.encoding.lower() not in ('utf-8', 'utf8'):
    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')

from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi.staticfiles import StaticFiles
from sqlalchemy.orm import Session
from contextlib import asynccontextmanager

from app.core.config import settings
from app.core.database import get_db, engine
from app.models import Base
from app.api.v1.api import api_router
from app.api.v1.endpoints import landed_costs
from app.api.v1.endpoints import branch_sales_realtime
import app.api.accounting_codes  # Force import to ensure router is loaded
from app.core.database import SessionLocal
from app.models.role import Permission
from scripts.seeds import registry as seed_registry  # Plan A seeds
import scripts.seeds.seed_roles_permissions  # noqa
import scripts.seeds.seed_units  # noqa
import scripts.seeds.seed_accounts  # noqa
import scripts.seeds.seed_walkin_customer  # noqa — global fallback walk-in customer
from sqlalchemy import text
from app.services.ifrs_reporting_service import IFRSReportingService

# Create database tables automatically only in development/error recovery mode
if settings.debug or settings.environment == "development":
    Base.metadata.create_all(bind=engine)

# Security
security = HTTPBearer()


def apply_ifrs_core_tags(db: Session) -> None:
    """Assign IFRS reporting_tag to accounting codes when missing or invalid."""
    from app.models.accounting import AccountingCode

    try:
        accounts = db.query(AccountingCode).all()
        updates = 0
        reviewed = 0
        manual_review = []

        for account in accounts:
            reviewed += 1
            current_tag = (account.reporting_tag or "").strip()
            recommended_tag = IFRSReportingService.determine_tag_for_account(account)
            current_valid = IFRSReportingService.validate_reporting_tag(current_tag)

            if recommended_tag and recommended_tag != current_tag:
                account.reporting_tag = recommended_tag
                updates += 1
            elif not current_valid and recommended_tag:
                account.reporting_tag = recommended_tag
                updates += 1
            elif not current_valid and not recommended_tag:
                manual_review.append(account)

        if updates:
            db.commit()

        remaining = db.query(AccountingCode).filter(
            (AccountingCode.reporting_tag.is_(None))
            | (AccountingCode.reporting_tag.notin_(IFRSReportingService.VALID_IFRS_TAGS))
        ).count()

        if updates:
            msg_suffix = (
                f", but {remaining} accounts still need manual classification"
                if remaining
                else ""
            )
            print(f"[IFRS] Assigned or corrected reporting tags for {updates} of {reviewed} accounts{msg_suffix}")
        elif remaining == 0:
            print("[IFRS] All accounting codes already have valid IFRS tags")

        if manual_review:
            preview = ", ".join(
                f"{acct.code} ({acct.name})"
                for acct in manual_review[:5]
            )
            extra = "..." if len(manual_review) > 5 else ""
            print(
                f"[IFRS] {len(manual_review)} accounts still need manual classification: {preview}{extra}"
            )
    except Exception as exc:
        db.rollback()
        print(f"[IFRS] Startup tagging failed: {exc}")


@asynccontextmanager
async def lifespan(app: FastAPI):
    """Application lifespan events"""
    from app.core.production import validate_production_config
    validate_production_config()
    # Startup
    print("Starting CNPERP ERP System...")
    # Ensure DB connection pool is fresh (important after enum/type migrations)
    try:
        engine.dispose()
        print("[INIT] Disposed SQLAlchemy engine pool to refresh DB type caches")
    except Exception as de:
        print(f"[INIT] Engine dispose warning: {de}")
    # Seed core POS permissions if missing
    db = SessionLocal()
    try:
        needed = [
            ("pos.record_sale","POS record sale","pos","record_sale","all"),
            ("pos.reconcile","POS reconciliation","pos","reconcile","all")
        ]
        existing = {p.name for p in db.query(Permission).filter(Permission.name.in_([n for n,_,_,_,_ in needed])).all()}
        new = 0
        for name, desc, module, action, resource in needed:
            if name not in existing:
                db.add(Permission(name=name, description=desc, module=module, action=action, resource=resource))
                new += 1
        if new:
            db.commit()
            print(f"[INIT] Seeded {new} POS permissions")
        # Run baseline seeds (system config only — NO users, NO branches, NO demo data)
        # Users and branches are created exclusively through the Company Setup Wizard.
        try:
            from scripts.seeds.registry import run_selected
            run_selected(db, ["roles_permissions", "units", "accounts", "walkin_customer"])
            print("[INIT] Baseline system seeds applied (roles, units, chart of accounts, walk-in customer)")
        except Exception as se:
            db.rollback()
            print(f"[INIT] Baseline seeding error: {se}")

        # Ensure app_settings singleton exists
        # TEMPORARILY DISABLED: The AppSetting model with 130+ columns causes
        # a huge SQL query that crashes startup. Will need to refactor this.
        # try:
        #     from app.models.app_setting import AppSetting
        #     instance = db.query(AppSetting).first()
        #     if not instance:
        #         print("[INIT] Creating app_settings singleton...")
        #         instance = AppSetting()
        #         db.add(instance)
        #         db.commit()
        #         print("[INIT] App settings created successfully")
        #     else:
        #         print("[INIT] App settings already exists")
        # except Exception as ase:
        #     print(f"[INIT] App settings creation error: {ase}")
        print("[INIT] Skipping app_settings check (temporarily disabled)")

        if settings.debug or settings.environment == "development":
            # Migrate accounting_codes unique index: (code) → (code, branch_id)
            # This allows per-company industry-filtered chart of accounts
            try:
                with engine.connect() as conn:
                    old_idx = conn.execute(text(
                        "SELECT 1 FROM pg_indexes WHERE tablename='accounting_codes' AND indexname='ix_accounting_codes_code'"
                    )).first()
                    new_idx = conn.execute(text(
                        "SELECT 1 FROM pg_indexes WHERE tablename='accounting_codes' AND indexname='ix_accounting_codes_code_branch'"
                    )).first()
                    if old_idx and not new_idx:
                        conn.execute(text("DROP INDEX IF EXISTS ix_accounting_codes_code"))
                        conn.execute(text(
                            "CREATE UNIQUE INDEX ix_accounting_codes_code_branch ON accounting_codes (code, branch_id) NULLS NOT DISTINCT"
                        ))
                        conn.commit()
                        print("[INIT] Migrated accounting_codes unique index to (code, branch_id)")
                    elif not new_idx:
                        conn.execute(text(
                            "CREATE UNIQUE INDEX IF NOT EXISTS ix_accounting_codes_code_branch ON accounting_codes (code, branch_id) NULLS NOT DISTINCT"
                        ))
                        conn.commit()
            except Exception as me:
                print(f"[INIT] accounting_codes index migration (non-fatal): {me}")

            # Ensure product_assemblies has unit_of_measure_id (idempotent quick migration)
            try:
                print("[INIT] Ensuring product_assemblies.unit_of_measure_id column exists...")
                with engine.connect() as conn:
                    exists = conn.execute(text(
                        "SELECT 1 FROM information_schema.columns WHERE table_name='product_assemblies' AND column_name='unit_of_measure_id'"
                    )).first()
                    if not exists:
                        conn.execute(text("ALTER TABLE product_assemblies ADD COLUMN IF NOT EXISTS unit_of_measure_id VARCHAR NULL"))
                        print("[INIT] Added column unit_of_measure_id to product_assemblies")
                    # Ensure FK exists
                    fk_exists = conn.execute(text(
                        "SELECT 1 FROM information_schema.table_constraints WHERE table_name='product_assemblies' AND constraint_name='fk_product_assemblies_unit_of_measure_id'"
                    )).first()
                    if not fk_exists:
                        conn.execute(text(
                            "ALTER TABLE product_assemblies ADD CONSTRAINT fk_product_assemblies_unit_of_measure_id FOREIGN KEY (unit_of_measure_id) REFERENCES unit_of_measures(id) ON DELETE SET NULL"
                        ))
                        print("[INIT] Added FK fk_product_assemblies_unit_of_measure_id")
                    conn.commit()
                print("[INIT] product_assemblies schema OK")
            except Exception as me:
                print(f"[INIT] Quick migration check failed (non-fatal): {me}")

            # Ensure billing tables exist (idempotent quick migration)
            try:
                print("[INIT] Ensuring billing tables exist...")
                with engine.connect() as conn:
                    # Check if billing_cycles table exists
                    cycles_exists = conn.execute(text(
                        "SELECT 1 FROM information_schema.tables WHERE table_name='billing_cycles'"
                    )).first()

                    if not cycles_exists:
                        print("[INIT] Billing tables not found, creating them...")
                        # Create billing tables using SQLAlchemy metadata
                        from app.models.billing import BillingCycle, BillableItem, RecurringInvoice, RecurringPayment
                        Base.metadata.create_all(bind=engine, tables=[
                            BillingCycle.__table__,
                            BillableItem.__table__,
                            RecurringInvoice.__table__,
                            RecurringPayment.__table__
                        ])
                        print("[INIT] Billing tables created successfully")
                    else:
                        print("[INIT] Billing tables already exist")
                print("[INIT] Billing schema OK")
            except Exception as be:
                print(f"[INIT] Billing tables check failed (non-fatal): {be}")
        else:
            print("[INIT] Skipping development-only schema adjustments")

        apply_ifrs_core_tags(db)

    except Exception as e:
        print(f"[INIT] Permission seeding failed: {e}")
    finally:
        db.close()

    from app.utils.cloud_sync_hooks import register_cloud_sync_listeners
    register_cloud_sync_listeners()
    print("[INIT] Cloud sync change capture registered")

    sync_task = None
    if settings.cloud_sync_enabled:
        from app.services.cloud_sync_service import is_lan_sync_client, cloud_sync_background_loop
        if is_lan_sync_client():
            import asyncio
            sync_task = asyncio.create_task(cloud_sync_background_loop())
            print("[INIT] Cloud sync background worker started (LAN/hybrid)")

    yield

    if sync_task:
        sync_task.cancel()
        print("[INIT] Cloud sync background worker stopped")
    # Shutdown
    print("Shutting down CNPERP ERP System...")


def _cors_origins() -> list:
    raw = (settings.cors_origins or "").strip()
    if raw:
        return [o.strip() for o in raw.split(",") if o.strip()]
    if settings.debug or (settings.environment or "").lower() in ("development", "dev", "local"):
        return ["http://localhost:8000", "http://127.0.0.1:8000", "http://localhost:8010", "http://127.0.0.1:8010"]
    return []


def create_application() -> FastAPI:
    """Create and configure FastAPI application"""
    _prod = (settings.environment or "").lower() in ("production", "prod") and not settings.debug
    app = FastAPI(
        title=settings.app_name,
        description="Comprehensive ERP System for Business Management",
        version="1.0.0",
        docs_url=None if _prod else "/docs",
        redoc_url=None if _prod else "/redoc",
        openapi_url=None if _prod else "/openapi.json",
        lifespan=lifespan,
    )

    # Add logging middleware FIRST (to catch all requests)
    from app.middleware.logging_middleware import RequestLoggingMiddleware, ErrorLoggingMiddleware
    from app.middleware.host_routing import HostRoutingMiddleware
    from app.middleware.cashier_access_control import CashierAccessControlMiddleware
    from app.middleware.tenant import TenantMiddleware
    from app.middleware.license import LicenseMiddleware
    from app.middleware.api_auth import ApiAuthMiddleware
    from app.core.security import SECRET_KEY, ALGORITHM
    app.add_middleware(HostRoutingMiddleware)
    app.add_middleware(ErrorLoggingMiddleware)
    app.add_middleware(RequestLoggingMiddleware)
    app.add_middleware(ApiAuthMiddleware)
    # Tenant middleware: extracts company_id from JWT into request.state
    app.add_middleware(TenantMiddleware, secret_key=SECRET_KEY, algorithm=ALGORITHM)
    app.add_middleware(LicenseMiddleware)
    # Add cashier access control middleware to restrict POS-only users
    app.add_middleware(CashierAccessControlMiddleware)

    # CORS — never use allow_origins=["*"] with credentials
    origins = _cors_origins()
    app.add_middleware(
        CORSMiddleware,
        allow_origins=origins if origins else ["https://localhost"],
        allow_credentials=True,
        allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
        allow_headers=["Authorization", "Content-Type", "X-Installer-Key", "X-CNP-License-Admin"],
        expose_headers=["X-Total-Count"],
    )

    # Mount static files
    import os
    # Use absolute path to static directory (app/static folder)
    static_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "static"))
    app.mount("/static", StaticFiles(directory=static_dir), name="static")


    # Include API routes
    app.include_router(api_router, prefix="/api/v1")
    app.include_router(landed_costs.router, prefix="/api/v1/landed-costs", tags=["Landed Costs"])
    app.include_router(branch_sales_realtime.router, tags=["Branch Sales"])

    # Include VAT router (use enhanced endpoints with actual data)
    from app.api.v1.endpoints import vat as vat_router
    app.include_router(vat_router.router, prefix="/api/v1/vat", tags=["VAT"])

    # Include Accounting Dimensions router
    from app.routers.accounting_dimensions import router as dimensions_router
    app.include_router(dimensions_router, tags=["Accounting Dimensions"])

    # Include Dimensional Reports router (replaces old reports system)
    from app.routers.dimensional_reports import router as dimensional_reports_router
    app.include_router(dimensional_reports_router, tags=["Dimensional Reports"])

    # Include Banking Dimensions router (Phase 4: Dimensional Banking)
    from app.routers.banking_dimensions import router as banking_dimensions_router
    app.include_router(banking_dimensions_router, tags=["Banking Dimensions"])

    # Include AI Insights router (Groq-powered financial analysis)
    from app.routers.ai_insights import router as ai_insights_router
    app.include_router(ai_insights_router, tags=["AI Insights"])

    # Include Dimensional Codes router (Phase 2: Code Generation Service)
    from app.routers.dimensional_codes import router as dimensional_codes_router
    app.include_router(dimensional_codes_router, tags=["Dimensional Codes"])

    # Include Fiscal Period Management router
    from app.routers.fiscal_period import router as fiscal_period_router
    app.include_router(fiscal_period_router, tags=["Fiscal Periods"])

    # Print all registered routes for diagnostics
    print("[DEBUG] Registered routes:")
    for route in app.routes:
        print(f"[DEBUG] {route.path} -> {getattr(route, 'methods', None)}")

    @app.get("/")
    async def root():
        from fastapi.responses import FileResponse
        import os
        static_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "static"))
        homepage_path = os.path.join(static_dir, "cnp-solutions.html")
        return FileResponse(homepage_path)

    @app.get("/home")
    async def marketing_home():
        from fastapi.responses import RedirectResponse
        return RedirectResponse(url="/", status_code=302)

    # Standardize 403 responses for frontend handling
    from fastapi.responses import JSONResponse
    from starlette.requests import Request
    from starlette.status import HTTP_403_FORBIDDEN

    @app.exception_handler(HTTPException)
    async def http_exception_handler(request: Request, exc: HTTPException):
        if exc.status_code == HTTP_403_FORBIDDEN:
            return JSONResponse(
                status_code=HTTP_403_FORBIDDEN,
                content={
                    "success": False,
                    "error": "forbidden",
                    "message": exc.detail or "Forbidden",
                    "path": request.url.path
                }
            )
        # default fallback
        return JSONResponse(
            status_code=exc.status_code,
            content={
                "success": False,
                "error": "http_error",
                "message": exc.detail,
                "path": request.url.path
            }
        )

    def _expose_error_detail_to_client(request: Request) -> bool:
        """Return real exception text for setup/licensing APIs (and when debug is on)."""
        if settings.debug or settings.expose_api_error_detail:
            return True
        path = request.url.path or ""
        setup_prefixes = (
            "/api/v1/companies/register",
            "/api/v1/licensing/",
        )
        return any(path.startswith(p) for p in setup_prefixes)

    @app.exception_handler(Exception)
    async def general_exception_handler(request: Request, exc: Exception):
        import traceback
        tb_str = traceback.format_exc()
        expose = _expose_error_detail_to_client(request)
        try:
            import logging as _log
            _log.getLogger("app.errors").error("%s %s: %s\n%s",
                request.method, request.url.path, exc, tb_str)
        except Exception:
            pass
        exc_msg = str(exc).strip() or exc.__class__.__name__
        body = {
            "success": False,
            "error": exc.__class__.__name__,
            "message": exc_msg if expose else "An internal error occurred",
            "detail": exc_msg if expose else None,
            "path": request.url.path,
        }
        if expose:
            body["traceback"] = tb_str[-3000:]
        return JSONResponse(status_code=500, content=body)

    @app.get("/health")
    async def health_check():
        return {"status": "healthy", "service": settings.app_name, "build": "asset-enum-fix-1"}

    @app.get("/health/db")
    async def health_db():
        """Database health check (no connection string in response)."""
        from sqlalchemy import text
        info = {"service": "database"}
        try:
            with engine.connect() as conn:
                conn.execute(text("SELECT 1"))
                info.update(status="ok", reachable=True)
        except Exception as e:
            info.update(status="error", reachable=False, error=str(e) if settings.debug else "unavailable")
        return info

    @app.get("/health/redis")
    async def redis_health():
        from app.core.cache import get_redis
        info = {"service": "redis"}
        try:
            r = get_redis()
            if r is None:
                info.update(status="unavailable", reachable=False)
            else:
                pong = r.ping()
                info.update(status="ok" if pong else "degraded", reachable=bool(pong))
                try:
                    info['dbsize'] = r.dbsize()
                except Exception:
                    pass
        except Exception as e:
            info.update(status="error", error=str(e), reachable=False)
        return info

    if not _prod:
        @app.get("/metrics")
        async def metrics():
            from prometheus_client import generate_latest, CONTENT_TYPE_LATEST
            from fastapi import Response
            data = generate_latest()
            return Response(content=data, media_type=CONTENT_TYPE_LATEST)

    @app.get("/setup")
    async def company_setup():
        """Company registration wizard — shown on first run before any company exists."""
        from fastapi.responses import FileResponse
        import os
        return FileResponse(os.path.abspath("app/static/company-setup.html"))

    @app.get("/install/local")
    async def local_install_page():
        """Local server installation guide and installer downloads."""
        from fastapi.responses import FileResponse
        import os
        return FileResponse(os.path.abspath("app/static/local-install.html"))

    @app.get("/updates")
    async def updates_page():
        """Application update management page."""
        from fastapi.responses import FileResponse
        import os
        return FileResponse(os.path.abspath("app/static/updates.html"))

    @app.get("/platform-admin")
    async def platform_admin_portal():
        """CNP Solutions operator portal — not linked from public marketing."""
        from fastapi.responses import FileResponse
        import os
        return FileResponse(os.path.abspath("app/static/platform-admin.html"))

    @app.get("/dashboard")
    async def dashboard():
        """Entry via marketing home; client routes authenticated users to the dashboard."""
        from fastapi.responses import RedirectResponse
        return RedirectResponse(url="/")

    @app.get("/backup-management")
    async def backup_management():
        """Serve the backup management page"""
        from fastapi.responses import FileResponse
        import os
        backup_path = os.path.abspath("app/static/backup-management.html")
        return FileResponse(backup_path)

    @app.get("/serial-numbers.html")
    async def serial_numbers():
        """Serve the serial numbers tracker page"""
        from fastapi.responses import FileResponse
        import os
        serial_path = os.path.abspath("app/static/serial-numbers.html")
        return FileResponse(serial_path)

    @app.get("/management-reports")
    async def management_reports():
        """Serve the management reports page"""
        from fastapi.responses import FileResponse
        import os
        reports_path = os.path.abspath("app/static/management-reports.html")
        return FileResponse(reports_path)

    @app.get("/role-management")
    async def role_management():
        """Serve the role management page"""
        from fastapi.responses import FileResponse
        import os
        role_path = os.path.abspath("app/static/role-management.html")
        return FileResponse(role_path)

    @app.get("/help-center")
    async def help_center():
        """Serve the help center page"""
        from fastapi.responses import FileResponse
        import os
        help_path = os.path.abspath("app/static/help-center.html")
        return FileResponse(help_path)

    @app.get("/complete-user-guide.html")
    async def complete_user_guide():
        """Serve the complete user guide page"""
        from fastapi.responses import FileResponse
        import os
        guide_path = os.path.abspath("app/static/complete-user-guide.html")
        return FileResponse(guide_path)

    @app.get("/invoice-template-designer")
    async def invoice_template_designer():
        """Serve the invoice template designer page"""
        from fastapi.responses import FileResponse
        import os
        designer_path = os.path.abspath("app/static/invoice-template-designer.html")
        return FileResponse(designer_path)

    @app.get("/test-static")
    async def test_static():
        """Test static file serving"""
        from fastapi.responses import FileResponse
        import os
        static_path = os.path.abspath("app/static/index.html")
        if os.path.exists(static_path):
            return FileResponse(static_path)
        else:
            return {"error": f"File not found: {static_path}", "cwd": os.getcwd()}

    @app.get("/frontend")
    async def frontend():
        """Serve the main frontend directly"""
        from fastapi.responses import FileResponse
        import os
        static_path = os.path.abspath("app/static/index.html")
        return FileResponse(static_path)

    if settings.debug:
        @app.get("/debug-static")
        async def debug_static():
            import os
            static_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "static"))
            return {"static_dir": static_dir, "exists": os.path.exists(static_dir)}

    return app


app = create_application()


# Global authentication dependency removed - authentication is now handled per-endpoint


if __name__ == "__main__":
    import uvicorn
    uvicorn.run(
        "app.main:app",
        host="0.0.0.0",
        port=8000,
        reload=settings.debug
    )
